diff options
Diffstat (limited to 'dom/script')
24 files changed, 8661 insertions, 0 deletions
diff --git a/dom/script/AutoEntryScript.cpp b/dom/script/AutoEntryScript.cpp new file mode 100644 index 0000000000..f1afa25b48 --- /dev/null +++ b/dom/script/AutoEntryScript.cpp @@ -0,0 +1,160 @@ +/* -*- 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/AutoEntryScript.h" + +#include <stdint.h> +#include <utility> +#include "js/ProfilingCategory.h" +#include "js/ProfilingStack.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsIDocShell.h" +#include "nsJSUtils.h" +#include "nsPIDOMWindow.h" +#include "nsPIDOMWindowInlines.h" +#include "nsString.h" +#include "xpcpublic.h" + +namespace mozilla::dom { + +namespace { +// Assert if it's not safe to run script. The helper class +// AutoAllowLegacyScriptExecution allows to allow-list +// legacy cases where it's actually not safe to run script. +#ifdef DEBUG +void AssertIfNotSafeToRunScript() { + // if it's safe to run script, then there is nothing to do here. + if (nsContentUtils::IsSafeToRunScript()) { + return; + } + + // auto allowing legacy script execution is fine for now. + if (AutoAllowLegacyScriptExecution::IsAllowed()) { + return; + } + + MOZ_ASSERT(false, "is it safe to run script?"); +} +#endif + +static unsigned long gRunToCompletionListeners = 0; + +} // namespace + +void UseEntryScriptProfiling() { + MOZ_ASSERT(NS_IsMainThread()); + ++gRunToCompletionListeners; +} + +void UnuseEntryScriptProfiling() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(gRunToCompletionListeners > 0); + --gRunToCompletionListeners; +} + +AutoEntryScript::AutoEntryScript(nsIGlobalObject* aGlobalObject, + const char* aReason, bool aIsMainThread) + : AutoJSAPI(aGlobalObject, aIsMainThread, eEntryScript), + mWebIDLCallerPrincipal(nullptr) + // This relies on us having a cx() because the AutoJSAPI constructor + // already ran. + , + mCallerOverride(cx()), + mAutoProfilerLabel( + "", aReason, JS::ProfilingCategoryPair::JS, + uint32_t(js::ProfilingStackFrame::Flags::RELEVANT_FOR_JS)), + mJSThreadExecution(aGlobalObject, aIsMainThread) { + MOZ_ASSERT(aGlobalObject); + + if (aIsMainThread) { +#ifdef DEBUG + AssertIfNotSafeToRunScript(); +#endif + if (gRunToCompletionListeners > 0) { + mDocShellEntryMonitor.emplace(cx(), aReason); + } + mScriptActivity.emplace(true); + } +} + +AutoEntryScript::AutoEntryScript(JSObject* aObject, const char* aReason, + bool aIsMainThread) + : AutoEntryScript(xpc::NativeGlobal(aObject), aReason, aIsMainThread) { + // xpc::NativeGlobal uses JS::GetNonCCWObjectGlobal, which asserts that + // aObject is not a CCW. +} + +AutoEntryScript::~AutoEntryScript() = default; + +AutoEntryScript::DocshellEntryMonitor::DocshellEntryMonitor(JSContext* aCx, + const char* aReason) + : JS::dbg::AutoEntryMonitor(aCx), mReason(aReason) {} + +void AutoEntryScript::DocshellEntryMonitor::Entry( + JSContext* aCx, JSFunction* aFunction, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, const char* aAsyncCause) { + JS::Rooted<JSFunction*> rootedFunction(aCx); + if (aFunction) { + rootedFunction = aFunction; + } + JS::Rooted<JSScript*> rootedScript(aCx); + if (aScript) { + rootedScript = aScript; + } + + nsCOMPtr<nsPIDOMWindowInner> window = xpc::CurrentWindowOrNull(aCx); + if (!window || !window->GetDocShell() || + !window->GetDocShell()->GetRecordProfileTimelineMarkers()) { + return; + } + + nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell(); + + nsAutoJSString functionName; + if (rootedFunction) { + JS::Rooted<JSString*> displayId(aCx, + JS_GetFunctionDisplayId(rootedFunction)); + if (displayId) { + if (!functionName.init(aCx, displayId)) { + JS_ClearPendingException(aCx); + return; + } + } + } + + nsString filename; + uint32_t lineNumber = 0; + if (!rootedScript) { + rootedScript = JS_GetFunctionScript(aCx, rootedFunction); + } + if (rootedScript) { + CopyUTF8toUTF16(MakeStringSpan(JS_GetScriptFilename(rootedScript)), + filename); + lineNumber = JS_GetScriptBaseLineNumber(aCx, rootedScript); + } + + if (!filename.IsEmpty() || !functionName.IsEmpty()) { + docShellForJSRunToCompletion->NotifyJSRunToCompletionStart( + mReason, functionName, filename, lineNumber, aAsyncStack, aAsyncCause); + } +} + +void AutoEntryScript::DocshellEntryMonitor::Exit(JSContext* aCx) { + nsCOMPtr<nsPIDOMWindowInner> window = xpc::CurrentWindowOrNull(aCx); + // Not really worth checking GetRecordProfileTimelineMarkers here. + if (window && window->GetDocShell()) { + nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell(); + docShellForJSRunToCompletion->NotifyJSRunToCompletionStop(); + } +} + +} // namespace mozilla::dom diff --git a/dom/script/AutoEntryScript.h b/dom/script/AutoEntryScript.h new file mode 100644 index 0000000000..20be83e4c6 --- /dev/null +++ b/dom/script/AutoEntryScript.h @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SCRIPT_AUTOENTRYSCRIPT_H_ +#define DOM_SCRIPT_AUTOENTRYSCRIPT_H_ + +#include "MainThreadUtils.h" +#include "js/Debug.h" +#include "js/TypeDecls.h" +#include "jsapi.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/dom/ScriptSettings.h" + +class nsIGlobalObject; +class nsIPrincipal; + +namespace xpc { +class AutoScriptActivity; +} + +namespace mozilla::dom { + +/* + * Static helpers in ScriptSettings which track the number of listeners + * of Javascript RunToCompletion events. These should be used by the code in + * nsDocShell::SetRecordProfileTimelineMarkers to indicate to script + * settings that script run-to-completion needs to be monitored. + * SHOULD BE CALLED ONLY BY MAIN THREAD. + */ +void UseEntryScriptProfiling(); +void UnuseEntryScriptProfiling(); + +/* + * A class that represents a new script entry point. + * + * |aReason| should be a statically-allocated C string naming the reason we're + * invoking JavaScript code: "setTimeout", "event", and so on. The devtools use + * these strings to label JS execution in timeline and profiling displays. + * + */ +class MOZ_STACK_CLASS AutoEntryScript : public AutoJSAPI { + public: + // Constructing the AutoEntryScript will ensure that it enters the + // Realm of aGlobalObject's JSObject and exposes that JSObject to active JS. + AutoEntryScript(nsIGlobalObject* aGlobalObject, const char* aReason, + bool aIsMainThread = NS_IsMainThread()); + + // aObject can be any object from the relevant global. It must not be a + // cross-compartment wrapper because CCWs are not associated with a single + // global. + // + // Constructing the AutoEntryScript will ensure that it enters the + // Realm of aObject JSObject and exposes aObject's global to active JS. + AutoEntryScript(JSObject* aObject, const char* aReason, + bool aIsMainThread = NS_IsMainThread()); + + ~AutoEntryScript(); + + void SetWebIDLCallerPrincipal(nsIPrincipal* aPrincipal) { + mWebIDLCallerPrincipal = aPrincipal; + } + + private: + // A subclass of AutoEntryMonitor that notifies the docshell. + class DocshellEntryMonitor final : public JS::dbg::AutoEntryMonitor { + public: + DocshellEntryMonitor(JSContext* aCx, const char* aReason); + + // Please note that |aAsyncCause| here is owned by the caller, and its + // lifetime must outlive the lifetime of the DocshellEntryMonitor object. + // In practice, |aAsyncCause| is identical to |aReason| passed into + // the AutoEntryScript constructor, so the lifetime requirements are + // trivially satisfied by |aReason| being a statically allocated string. + void Entry(JSContext* aCx, JSFunction* aFunction, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) override { + Entry(aCx, aFunction, nullptr, aAsyncStack, aAsyncCause); + } + + void Entry(JSContext* aCx, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) override { + Entry(aCx, nullptr, aScript, aAsyncStack, aAsyncCause); + } + + void Exit(JSContext* aCx) override; + + private: + void Entry(JSContext* aCx, JSFunction* aFunction, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, const char* aAsyncCause); + + const char* mReason; + }; + + // It's safe to make this a weak pointer, since it's the subject principal + // when we go on the stack, so can't go away until after we're gone. In + // particular, this is only used from the CallSetup constructor, and only in + // the aIsJSImplementedWebIDL case. And in that case, the subject principal + // is the principal of the callee function that is part of the CallArgs just a + // bit up the stack, and which will outlive us. So we know the principal + // can't go away until then either. + nsIPrincipal* MOZ_NON_OWNING_REF mWebIDLCallerPrincipal; + friend nsIPrincipal* GetWebIDLCallerPrincipal(); + + Maybe<DocshellEntryMonitor> mDocShellEntryMonitor; + Maybe<xpc::AutoScriptActivity> mScriptActivity; + JS::AutoHideScriptedCaller mCallerOverride; + AutoProfilerLabel mAutoProfilerLabel; + AutoRequestJSThreadExecution mJSThreadExecution; +}; + +} // namespace mozilla::dom + +#endif // DOM_SCRIPT_AUTOENTRYSCRIPT_H_ diff --git a/dom/script/ModuleLoader.cpp b/dom/script/ModuleLoader.cpp new file mode 100644 index 0000000000..6cb9bf0245 --- /dev/null +++ b/dom/script/ModuleLoader.cpp @@ -0,0 +1,318 @@ +/* -*- 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 "ScriptLoader.h" +#include "ModuleLoader.h" + +#include "jsapi.h" +#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions +#include "js/ContextOptions.h" // JS::ContextOptionsRef +#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil +#include "js/MemoryFunctions.h" +#include "js/Modules.h" // JS::FinishDynamicModuleImport, JS::{G,S}etModuleResolveHook, JS::Get{ModulePrivate,ModuleScript,RequestedModule{s,Specifier,SourcePos}}, JS::SetModule{DynamicImport,Metadata}Hook +#include "js/OffThreadScriptCompilation.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/Realm.h" +#include "js/SourceText.h" +#include "js/loader/LoadedScript.h" +#include "js/loader/ScriptLoadRequest.h" +#include "js/loader/ModuleLoaderBase.h" +#include "js/loader/ModuleLoadRequest.h" +#include "xpcpublic.h" +#include "GeckoProfiler.h" +#include "nsContentSecurityManager.h" +#include "nsIContent.h" +#include "nsJSUtils.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsGlobalWindowInner.h" +#include "nsIPrincipal.h" +#include "mozilla/LoadInfo.h" + +using JS::SourceText; +using namespace JS::loader; + +namespace mozilla::dom { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) + +////////////////////////////////////////////////////////////// +// DOM module loader +////////////////////////////////////////////////////////////// + +ModuleLoader::ModuleLoader(ScriptLoader* aLoader, + nsIGlobalObject* aGlobalObject, Kind aKind) + : ModuleLoaderBase(aLoader, aGlobalObject), mKind(aKind) {} + +ScriptLoader* ModuleLoader::GetScriptLoader() { + return static_cast<ScriptLoader*>(mLoader.get()); +} + +bool ModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) { + if (!GetScriptLoader()->GetDocument()) { + *aRvOut = NS_ERROR_NULL_POINTER; + return false; + } + + // If this document is sandboxed without 'allow-scripts', abort. + if (GetScriptLoader()->GetDocument()->HasScriptsBlockedBySandbox()) { + *aRvOut = NS_OK; + return false; + } + + // To prevent dynamic code execution, content scripts can only + // load moz-extension URLs. + nsCOMPtr<nsIPrincipal> principal = aRequest->TriggeringPrincipal(); + if (BasePrincipal::Cast(principal)->ContentScriptAddonPolicy() && + !aRequest->mURI->SchemeIs("moz-extension")) { + *aRvOut = NS_ERROR_DOM_WEBEXT_CONTENT_SCRIPT_URI; + return false; + } + + if (LOG_ENABLED()) { + nsAutoCString url; + aRequest->mURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Start Module Load (url = %s)", aRequest, + url.get())); + } + + return true; +} + +nsresult ModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { + // According to the spec, module scripts have different behaviour to classic + // scripts and always use CORS. Only exception: Non linkable about: pages + // which load local module scripts. + bool isAboutPageLoadingChromeURI = ScriptLoader::IsAboutPageLoadingChromeURI( + aRequest, GetScriptLoader()->GetDocument()); + + nsContentSecurityManager::CORSSecurityMapping corsMapping = + isAboutPageLoadingChromeURI + ? nsContentSecurityManager::CORSSecurityMapping::DISABLE_CORS_CHECKS + : nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS; + + nsSecurityFlags securityFlags = + nsContentSecurityManager::ComputeSecurityFlags(aRequest->CORSMode(), + corsMapping); + + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + // Delegate Shared Behavior to base ScriptLoader + // + // aCharsetForPreload is passed as Nothing() because this is not a preload + // and `StartLoadInternal` is able to find the charset by using `aRequest` + // for this case. + nsresult rv = GetScriptLoader()->StartLoadInternal( + aRequest, securityFlags, 0, Nothing() /* aCharsetForPreload */); + NS_ENSURE_SUCCESS(rv, rv); + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-an-import()-module-script-graph + // Step 1. Disallow further import maps given settings object. + if (!aRequest->GetScriptLoadContext()->IsPreload()) { + LOG(("ScriptLoadRequest (%p): Disallow further import maps.", aRequest)); + DisallowImportMaps(); + } + + LOG(("ScriptLoadRequest (%p): Start fetching module", aRequest)); + + return NS_OK; +} + +void ModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsReadyToRun()); + + if (aRequest->IsTopLevel()) { + if (aRequest->GetScriptLoadContext()->mIsInline && + aRequest->GetScriptLoadContext()->GetParserCreated() == + NOT_FROM_PARSER) { + GetScriptLoader()->RunScriptWhenSafe(aRequest); + } else { + GetScriptLoader()->MaybeMoveToLoadedList(aRequest); + GetScriptLoader()->ProcessPendingRequests(); + } + } + + aRequest->GetScriptLoadContext()->MaybeUnblockOnload(); +} + +nsresult ModuleLoader::CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions, + ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) { + if (aRequest->GetScriptLoadContext()->mWasCompiledOMT) { + JS::Rooted<JS::InstantiationStorage> storage(aCx); + RefPtr<JS::Stencil> stencil = JS::FinishOffThreadStencil( + aCx, aRequest->GetScriptLoadContext()->mOffThreadToken, + storage.address()); + + aRequest->GetScriptLoadContext()->mOffThreadToken = nullptr; + + if (!stencil) { + return NS_ERROR_FAILURE; + } + + JS::InstantiateOptions instantiateOptions(aOptions); + aModuleOut.set(JS::InstantiateModuleStencil(aCx, instantiateOptions, + stencil, storage.address())); + if (!aModuleOut) { + return NS_ERROR_FAILURE; + } + + if (aRequest->IsTextSource() && + ScriptLoader::ShouldCacheBytecode(aRequest)) { + if (!JS::StartIncrementalEncoding(aCx, std::move(stencil))) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; + } + + if (!nsJSUtils::IsScriptable(aGlobal)) { + return NS_ERROR_FAILURE; + } + + RefPtr<JS::Stencil> stencil; + if (aRequest->IsTextSource()) { + MaybeSourceText maybeSource; + nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource); + NS_ENSURE_SUCCESS(rv, rv); + + auto compile = [&](auto& source) { + return JS::CompileModuleScriptToStencil(aCx, aOptions, source); + }; + stencil = maybeSource.mapNonEmpty(compile); + } else { + MOZ_ASSERT(aRequest->IsBytecode()); + JS::DecodeOptions decodeOptions(aOptions); + decodeOptions.borrowBuffer = true; + + auto& bytecode = aRequest->mScriptBytecode; + auto& offset = aRequest->mBytecodeOffset; + + JS::TranscodeRange range(bytecode.begin() + offset, + bytecode.length() - offset); + + JS::TranscodeResult tr = + JS::DecodeStencil(aCx, decodeOptions, range, getter_AddRefs(stencil)); + if (tr != JS::TranscodeResult::Ok) { + return NS_ERROR_DOM_JS_DECODING_ERROR; + } + } + + if (!stencil) { + return NS_ERROR_FAILURE; + } + + JS::InstantiateOptions instantiateOptions(aOptions); + aModuleOut.set( + JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); + if (!aModuleOut) { + return NS_ERROR_FAILURE; + } + + if (aRequest->IsTextSource() && ScriptLoader::ShouldCacheBytecode(aRequest)) { + if (!JS::StartIncrementalEncoding(aCx, std::move(stencil))) { + return NS_ERROR_FAILURE; + } + } + + return NS_OK; +} + +/* static */ +already_AddRefed<ModuleLoadRequest> ModuleLoader::CreateTopLevel( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, ScriptLoader* aLoader, + ScriptLoadContext* aContext) { + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aFetchOptions, aIntegrity, aReferrer, aContext, true, + /* is top level */ false, /* is dynamic import */ + aLoader->GetModuleLoader(), + ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr); + + return request.forget(); +} + +already_AddRefed<ModuleLoadRequest> ModuleLoader::CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) { + RefPtr<ScriptLoadContext> newContext = new ScriptLoadContext(); + newContext->mIsInline = false; + // Propagated Parent values. TODO: allow child modules to use root module's + // script mode. + newContext->mScriptMode = aParent->GetScriptLoadContext()->mScriptMode; + + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aParent->mFetchOptions, SRIMetadata(), aParent->mURI, newContext, + false, /* is top level */ + false, /* is dynamic import */ + aParent->mLoader, aParent->mVisitedSet, aParent->GetRootModule()); + + return request.forget(); +} + +already_AddRefed<ModuleLoadRequest> ModuleLoader::CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JS::Value> aReferencingPrivate, JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) { + MOZ_ASSERT(aSpecifier); + MOZ_ASSERT(aPromise); + + RefPtr<ScriptFetchOptions> options = nullptr; + nsIURI* baseURL = nullptr; + RefPtr<ScriptLoadContext> context = new ScriptLoadContext(); + + if (aMaybeActiveScript) { + options = aMaybeActiveScript->GetFetchOptions(); + baseURL = aMaybeActiveScript->BaseURL(); + } else { + // We don't have a referencing script so fall back on using + // options from the document. This can happen when the user + // triggers an inline event handler, as there is no active script + // there. + Document* document = GetScriptLoader()->GetDocument(); + + nsCOMPtr<nsIPrincipal> principal = GetGlobalObject()->PrincipalOrNull(); + MOZ_ASSERT_IF(GetKind() == WebExtension, + BasePrincipal::Cast(principal)->ContentScriptAddonPolicy()); + MOZ_ASSERT_IF(GetKind() == Normal, principal == document->NodePrincipal()); + + options = new ScriptFetchOptions( + mozilla::CORS_NONE, document->GetReferrerPolicy(), principal, nullptr); + baseURL = document->GetDocBaseURI(); + } + + context->mIsInline = false; + context->mScriptMode = ScriptLoadContext::ScriptMode::eAsync; + + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, options, SRIMetadata(), baseURL, context, true, + /* is top level */ true, /* is dynamic import */ + this, ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr); + + request->mDynamicReferencingPrivate = aReferencingPrivate; + request->mDynamicSpecifier = aSpecifier; + request->mDynamicPromise = aPromise; + + HoldJSObjects(request.get()); + + return request.forget(); +} + +ModuleLoader::~ModuleLoader() { + LOG(("ModuleLoader::~ModuleLoader %p", this)); + mLoader = nullptr; +} + +#undef LOG +#undef LOG_ENABLED + +} // namespace mozilla::dom diff --git a/dom/script/ModuleLoader.h b/dom/script/ModuleLoader.h new file mode 100644 index 0000000000..ff37a31999 --- /dev/null +++ b/dom/script/ModuleLoader.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ModuleLoader_h +#define mozilla_dom_ModuleLoader_h + +#include "mozilla/dom/ScriptLoadContext.h" +#include "js/loader/ModuleLoaderBase.h" +#include "js/loader/ScriptLoadRequest.h" +#include "ScriptLoader.h" + +class nsIURI; + +namespace JS { + +class CompileOptions; + +namespace loader { + +class ModuleLoadRequest; + +} // namespace loader +} // namespace JS + +namespace mozilla::dom { + +class ScriptLoader; +class SRIMetadata; + +////////////////////////////////////////////////////////////// +// DOM Module loader implementation +////////////////////////////////////////////////////////////// + +class ModuleLoader final : public JS::loader::ModuleLoaderBase { + private: + virtual ~ModuleLoader(); + + public: + enum Kind { Normal, WebExtension }; + + ModuleLoader(ScriptLoader* aLoader, nsIGlobalObject* aGlobalObject, + Kind aKind); + + Kind GetKind() const { return mKind; } + + ScriptLoader* GetScriptLoader(); + + bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override; + + nsresult StartFetch(ModuleLoadRequest* aRequest) override; + + void OnModuleLoadComplete(ModuleLoadRequest* aRequest) override; + + nsresult CompileFetchedModule( + JSContext* aCx, JS::Handle<JSObject*> aGlobal, + JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, + JS::MutableHandle<JSObject*> aModuleScript) override; + + // Create a top-level module load request. + static already_AddRefed<ModuleLoadRequest> CreateTopLevel( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, ScriptLoader* aLoader, + ScriptLoadContext* aContext); + + // Create a module load request for a static module import. + already_AddRefed<ModuleLoadRequest> CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) override; + + // Create a module load request for a dynamic module import. + already_AddRefed<ModuleLoadRequest> CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) override; + + static ModuleLoader* From(ModuleLoaderBase* aLoader) { + return static_cast<ModuleLoader*>(aLoader); + } + + private: + const Kind mKind; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ModuleLoader_h diff --git a/dom/script/ScriptCompression.cpp b/dom/script/ScriptCompression.cpp new file mode 100644 index 0000000000..7aa8f08de2 --- /dev/null +++ b/dom/script/ScriptCompression.cpp @@ -0,0 +1,174 @@ +/* -*- 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 "zlib.h" +#include "ScriptLoadRequest.h" +#include "ScriptLoader.h" +#include "mozilla/PerfStats.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/Vector.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_browser.h" + +using namespace mozilla; + +namespace JS::loader { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(mozilla::dom::ScriptLoader::gScriptLoaderLog, \ + mozilla::LogLevel::Debug, args) + +/* + * ScriptBytecodeDataLayout + * + * ScriptBytecodeDataLayout provides accessors to maintain the correct data + * layout of the ScriptLoadRequest's script bytecode buffer. + */ +class ScriptBytecodeDataLayout { + public: + explicit ScriptBytecodeDataLayout(mozilla::Vector<uint8_t>& aBytecode, + size_t aBytecodeOffset) + : mBytecode(aBytecode), mBytecodeOffset(aBytecodeOffset) {} + + uint8_t* prelude() const { return mBytecode.begin(); } + size_t preludeLength() const { return mBytecodeOffset; } + + uint8_t* bytecode() const { return prelude() + mBytecodeOffset; } + size_t bytecodeLength() const { return mBytecode.length() - preludeLength(); } + + mozilla::Vector<uint8_t>& mBytecode; + size_t mBytecodeOffset; +}; + +/* + * ScriptBytecodeCompressedDataLayout + * + * ScriptBytecodeCompressedDataLayout provides accessors to maintain the correct + * data layout of a compressed script bytecode buffer. + */ +class ScriptBytecodeCompressedDataLayout { + public: + using UncompressedLengthType = uint32_t; + + explicit ScriptBytecodeCompressedDataLayout( + mozilla::Vector<uint8_t>& aBytecode, size_t aBytecodeOffset) + : mBytecode(aBytecode), mBytecodeOffset(aBytecodeOffset) {} + + uint8_t* prelude() const { return mBytecode.begin(); } + size_t preludeLength() const { return mBytecodeOffset; } + + uint8_t* uncompressedLength() const { return prelude() + mBytecodeOffset; } + size_t uncompressedLengthLength() const { + return sizeof(UncompressedLengthType); + } + + uint8_t* bytecode() const { + return uncompressedLength() + uncompressedLengthLength(); + } + size_t bytecodeLength() const { + return mBytecode.length() - uncompressedLengthLength() - preludeLength(); + } + + mozilla::Vector<uint8_t>& mBytecode; + size_t mBytecodeOffset; +}; + +bool ScriptBytecodeCompress(Vector<uint8_t>& aBytecodeBuf, + size_t aBytecodeOffset, + Vector<uint8_t>& aCompressedBytecodeBufOut) { + // TODO probably need to move this to a helper thread + + AUTO_PROFILER_MARKER_TEXT("ScriptBytecodeCompress", JS, {}, ""_ns); + PerfStats::AutoMetricRecording<PerfStats::Metric::JSBC_Compression> + autoRecording; + + ScriptBytecodeDataLayout uncompressedLayout(aBytecodeBuf, aBytecodeOffset); + ScriptBytecodeCompressedDataLayout compressedLayout( + aCompressedBytecodeBufOut, uncompressedLayout.preludeLength()); + ScriptBytecodeCompressedDataLayout::UncompressedLengthType + uncompressedLength = uncompressedLayout.bytecodeLength(); + z_stream zstream{.next_in = uncompressedLayout.bytecode(), + .avail_in = uncompressedLength}; + auto compressedLength = deflateBound(&zstream, uncompressedLength); + if (!aCompressedBytecodeBufOut.resizeUninitialized( + compressedLength + compressedLayout.preludeLength() + + compressedLayout.uncompressedLengthLength())) { + return false; + } + memcpy(compressedLayout.prelude(), uncompressedLayout.prelude(), + uncompressedLayout.preludeLength()); + memcpy(compressedLayout.uncompressedLength(), &uncompressedLength, + sizeof(uncompressedLength)); + zstream.next_out = compressedLayout.bytecode(); + zstream.avail_out = compressedLength; + + const uint32_t compressionLevel = + StaticPrefs::browser_cache_jsbc_compression_level(); + if (deflateInit(&zstream, compressionLevel) != Z_OK) { + LOG( + ("ScriptLoadRequest: Unable to initialize bytecode cache " + "compression.")); + return false; + } + auto autoDestroy = MakeScopeExit([&]() { deflateEnd(&zstream); }); + + int ret = deflate(&zstream, Z_FINISH); + if (ret == Z_MEM_ERROR) { + return false; + } + MOZ_RELEASE_ASSERT(ret == Z_STREAM_END); + + aCompressedBytecodeBufOut.shrinkTo(zstream.next_out - + aCompressedBytecodeBufOut.begin()); + return true; +} + +bool ScriptBytecodeDecompress(Vector<uint8_t>& aCompressedBytecodeBuf, + size_t aBytecodeOffset, + Vector<uint8_t>& aBytecodeBufOut) { + AUTO_PROFILER_MARKER_TEXT("ScriptBytecodeDecompress", JS, {}, ""_ns); + PerfStats::AutoMetricRecording<PerfStats::Metric::JSBC_Decompression> + autoRecording; + + ScriptBytecodeDataLayout uncompressedLayout(aBytecodeBufOut, aBytecodeOffset); + ScriptBytecodeCompressedDataLayout compressedLayout( + aCompressedBytecodeBuf, uncompressedLayout.preludeLength()); + ScriptBytecodeCompressedDataLayout::UncompressedLengthType uncompressedLength; + memcpy(&uncompressedLength, compressedLayout.uncompressedLength(), + compressedLayout.uncompressedLengthLength()); + if (!aBytecodeBufOut.resizeUninitialized(uncompressedLayout.preludeLength() + + uncompressedLength)) { + return false; + } + memcpy(uncompressedLayout.prelude(), compressedLayout.prelude(), + compressedLayout.preludeLength()); + + z_stream zstream{nullptr}; + zstream.next_in = compressedLayout.bytecode(); + zstream.avail_in = static_cast<uint32_t>(compressedLayout.bytecodeLength()); + zstream.next_out = uncompressedLayout.bytecode(); + zstream.avail_out = uncompressedLength; + if (inflateInit(&zstream) != Z_OK) { + LOG(("ScriptLoadRequest: inflateInit FAILED (%s)", zstream.msg)); + return false; + } + auto autoDestroy = MakeScopeExit([&]() { inflateEnd(&zstream); }); + + int ret = inflate(&zstream, Z_NO_FLUSH); + bool ok = (ret == Z_OK || ret == Z_STREAM_END) && zstream.avail_in == 0; + if (!ok) { + LOG(("ScriptLoadReques: inflate FAILED (%s)", zstream.msg)); + return false; + } + + return true; +} + +#undef LOG + +} // namespace JS::loader diff --git a/dom/script/ScriptCompression.h b/dom/script/ScriptCompression.h new file mode 100644 index 0000000000..c824aa9c15 --- /dev/null +++ b/dom/script/ScriptCompression.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef js_loader_ScriptCompression_h +#define js_loader_ScriptCompression_h + +#include "ErrorList.h" +#include "mozilla/Vector.h" + +namespace JS::loader { + +class ScriptLoadRequest; + +/** + * Compress the bytecode stored in a buffer. All data before the bytecode is + * copied into the output buffer without modification. + * + * @param aBytecodeBuf buffer containing the uncompressed bytecode + * @param aBytecodeOffset offset of the bytecode in the buffer + * @param aCompressedBytecodeBufOut buffer to store the compressed bytecode in + */ +bool ScriptBytecodeCompress( + mozilla::Vector<uint8_t>& aBytecodeBuf, size_t aBytecodeOffset, + mozilla::Vector<uint8_t>& aCompressedBytecodeBufOut); + +/** + * Uncompress the bytecode stored in a buffer. All data before the bytecode is + * copied into the output buffer without modification. + * + * @param aCompressedBytecodeBuf buffer containing the compressed bytecode + * @param aBytecodeOffset offset of the bytecode in the buffer + * @param aBytecodeBufOut buffer to store the uncompressed bytecode in + */ +bool ScriptBytecodeDecompress(mozilla::Vector<uint8_t>& aCompressedBytecodeBuf, + size_t aBytecodeOffset, + mozilla::Vector<uint8_t>& aBytecodeBufOut); + +} // namespace JS::loader + +#endif // js_loader_ScriptCompression_h diff --git a/dom/script/ScriptDecoding.h b/dom/script/ScriptDecoding.h new file mode 100644 index 0000000000..ea4c4aeef3 --- /dev/null +++ b/dom/script/ScriptDecoding.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +/* Script decoding templates for processing byte data as UTF-8 or UTF-16. */ + +#ifndef mozilla_dom_ScriptDecoding_h +#define mozilla_dom_ScriptDecoding_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/CheckedInt.h" // mozilla::CheckedInt +#include "mozilla/Encoding.h" // mozilla::Decoder +#include "mozilla/Span.h" // mozilla::Span +#include "mozilla/UniquePtr.h" // mozilla::UniquePtr + +#include <stddef.h> // size_t +#include <stdint.h> // uint8_t, uint32_t +#include <type_traits> // std::is_same + +namespace mozilla::dom { + +template <typename Unit> +struct ScriptDecoding { + static_assert(std::is_same<Unit, char16_t>::value || + std::is_same<Unit, Utf8Unit>::value, + "must be either UTF-8 or UTF-16"); +}; + +template <> +struct ScriptDecoding<char16_t> { + static CheckedInt<size_t> MaxBufferLength(const UniquePtr<Decoder>& aDecoder, + uint32_t aByteLength) { + return aDecoder->MaxUTF16BufferLength(aByteLength); + } + + static size_t DecodeInto(const UniquePtr<Decoder>& aDecoder, + const Span<const uint8_t>& aSrc, + Span<char16_t> aDest, bool aEndOfSource) { + uint32_t result; + size_t read; + size_t written; + std::tie(result, read, written, std::ignore) = + aDecoder->DecodeToUTF16(aSrc, aDest, aEndOfSource); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aSrc.Length()); + MOZ_ASSERT(written <= aDest.Length()); + + return written; + } +}; + +template <> +struct ScriptDecoding<Utf8Unit> { + static CheckedInt<size_t> MaxBufferLength(const UniquePtr<Decoder>& aDecoder, + uint32_t aByteLength) { + return aDecoder->MaxUTF8BufferLength(aByteLength); + } + + static size_t DecodeInto(const UniquePtr<Decoder>& aDecoder, + const Span<const uint8_t>& aSrc, + Span<Utf8Unit> aDest, bool aEndOfSource) { + uint32_t result; + size_t read; + size_t written; + // Until C++ char8_t happens, our decoder APIs deal in |uint8_t| while + // |Utf8Unit| internally deals with |char|, so there's inevitable impedance + // mismatch between |aDest| as |Utf8Unit| and |AsWritableBytes(aDest)| as + // |Span<uint8_t>|. :-( The written memory will be interpreted through + // |char Utf8Unit::mValue| which is *permissible* because any object's + // memory can be interpreted as |char|. Unfortunately, until + // twos-complement is mandated, we have to play fast and loose and *hope* + // interpreting memory storing |uint8_t| as |char| will pick up the desired + // wrapped-around value. ¯\_(ツ)_/¯ + std::tie(result, read, written, std::ignore) = + aDecoder->DecodeToUTF8(aSrc, AsWritableBytes(aDest), aEndOfSource); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aSrc.Length()); + MOZ_ASSERT(written <= aDest.Length()); + + return written; + } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ScriptDecoding_h diff --git a/dom/script/ScriptElement.cpp b/dom/script/ScriptElement.cpp new file mode 100644 index 0000000000..d242888ce3 --- /dev/null +++ b/dom/script/ScriptElement.cpp @@ -0,0 +1,184 @@ +/* -*- 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 "ScriptElement.h" +#include "ScriptLoader.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsContentUtils.h" +#include "nsThreadUtils.h" +#include "nsPresContext.h" +#include "nsIParser.h" +#include "nsGkAtoms.h" +#include "nsContentSink.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMETHODIMP +ScriptElement::ScriptAvailable(nsresult aResult, nsIScriptElement* aElement, + bool aIsInlineClassicScript, nsIURI* aURI, + uint32_t aLineNo) { + if (!aIsInlineClassicScript && NS_FAILED(aResult)) { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->IncrementScriptNestingLevel(); + } + nsresult rv = FireErrorEvent(); + if (parser) { + parser->DecrementScriptNestingLevel(); + } + return rv; + } + return NS_OK; +} + +/* virtual */ +nsresult ScriptElement::FireErrorEvent() { + nsIContent* cont = GetAsContent(); + + return nsContentUtils::DispatchTrustedEvent( + cont->OwnerDoc(), cont, u"error"_ns, CanBubble::eNo, Cancelable::eNo); +} + +NS_IMETHODIMP +ScriptElement::ScriptEvaluated(nsresult aResult, nsIScriptElement* aElement, + bool aIsInline) { + nsresult rv = NS_OK; + if (!aIsInline) { + nsCOMPtr<nsIContent> cont = GetAsContent(); + + RefPtr<nsPresContext> presContext = + nsContentUtils::GetContextForContent(cont); + + nsEventStatus status = nsEventStatus_eIgnore; + EventMessage message = NS_SUCCEEDED(aResult) ? eLoad : eLoadError; + WidgetEvent event(true, message); + // Load event doesn't bubble. + event.mFlags.mBubbles = (message != eLoad); + + EventDispatcher::Dispatch(cont, presContext, &event, nullptr, &status); + } + + return rv; +} + +void ScriptElement::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + MaybeProcessScript(); +} + +void ScriptElement::AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + // https://html.spec.whatwg.org/#script-processing-model + // When a script element el that is not parser-inserted experiences one of the + // events listed in the following list, the user agent must immediately + // prepare the script element el: + // - The script element is connected and has a src attribute set where + // previously the element had no such attribute. + if (aElement->IsSVGElement() && ((aNameSpaceID != kNameSpaceID_XLink && + aNameSpaceID != kNameSpaceID_None) || + aAttribute != nsGkAtoms::href)) { + return; + } + if (aElement->IsHTMLElement() && + (aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::src)) { + return; + } + if (mParserCreated == NOT_FROM_PARSER && + aModType == MutationEvent_Binding::ADDITION) { + auto* cont = GetAsContent(); + if (cont->IsInComposedDoc()) { + MaybeProcessScript(); + } + } +} + +void ScriptElement::ContentAppended(nsIContent* aFirstNewContent) { + MaybeProcessScript(); +} + +void ScriptElement::ContentInserted(nsIContent* aChild) { + MaybeProcessScript(); +} + +bool ScriptElement::MaybeProcessScript() { + nsIContent* cont = GetAsContent(); + + NS_ASSERTION(cont->DebugGetSlots()->mMutationObservers.contains(this), + "You forgot to add self as observer"); + + // https://html.spec.whatwg.org/#parsing-main-incdata + // An end tag whose tag name is "script" + // - If the active speculative HTML parser is null and the JavaScript + // execution context stack is empty, then perform a microtask checkpoint. + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; })); + + if (mAlreadyStarted || !mDoneAddingChildren || !cont->GetComposedDoc() || + mMalformed) { + return false; + } + + if (!HasScriptContent()) { + // In the case of an empty, non-external classic script, there is nothing + // to process. However, we must perform a microtask checkpoint afterwards, + // as per https://html.spec.whatwg.org/#clean-up-after-running-script + if (mKind == JS::loader::ScriptKind::eClassic && !mExternal) { + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; })); + } + return false; + } + + // Check the type attribute to determine language and version. If type exists, + // it trumps the deprecated 'language='. + nsAutoString type; + bool hasType = GetScriptType(type); + if (!type.IsEmpty()) { + NS_ENSURE_TRUE(nsContentUtils::IsJavascriptMIMEType(type) || + type.LowerCaseEqualsASCII("module") || + type.LowerCaseEqualsASCII("importmap"), + false); + } else if (!hasType) { + // "language" is a deprecated attribute of HTML, so we check it only for + // HTML script elements. + if (cont->IsHTMLElement()) { + nsAutoString language; + cont->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::language, + language); + if (!language.IsEmpty() && + !nsContentUtils::IsJavaScriptLanguage(language)) { + return false; + } + } + } + + Document* ownerDoc = cont->OwnerDoc(); + FreezeExecutionAttrs(ownerDoc); + + mAlreadyStarted = true; + + nsCOMPtr<nsIParser> parser = ((nsIScriptElement*)this)->GetCreatorParser(); + if (parser) { + nsCOMPtr<nsIContentSink> sink = parser->GetContentSink(); + if (sink) { + nsCOMPtr<Document> parserDoc = do_QueryInterface(sink->GetTarget()); + if (ownerDoc != parserDoc) { + // Willful violation of HTML5 as of 2010-12-01 + return false; + } + } + } + + RefPtr<ScriptLoader> loader = ownerDoc->ScriptLoader(); + return loader->ProcessScriptElement(this, type); +} diff --git a/dom/script/ScriptElement.h b/dom/script/ScriptElement.h new file mode 100644 index 0000000000..241ae67eb4 --- /dev/null +++ b/dom/script/ScriptElement.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ScriptElement_h +#define mozilla_dom_ScriptElement_h + +#include "mozilla/Attributes.h" +#include "nsIScriptLoaderObserver.h" +#include "nsIScriptElement.h" +#include "nsStubMutationObserver.h" + +namespace mozilla::dom { + +/** + * Baseclass useful for script elements (such as <xhtml:script> and + * <svg:script>). Currently the class assumes that only the 'src' + * attribute and the children of the class affect what script to execute. + */ + +class ScriptElement : public nsIScriptElement, public nsStubMutationObserver { + public: + // nsIScriptLoaderObserver + NS_DECL_NSISCRIPTLOADEROBSERVER + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + + explicit ScriptElement(FromParser aFromParser) + : nsIScriptElement(aFromParser) {} + + virtual nsresult FireErrorEvent() override; + + protected: + // Internal methods + + /** + * Check if this element contains any script, linked or inline + */ + virtual bool HasScriptContent() = 0; + + virtual bool MaybeProcessScript() override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ScriptElement_h diff --git a/dom/script/ScriptLoadContext.cpp b/dom/script/ScriptLoadContext.cpp new file mode 100644 index 0000000000..8bc2275ae3 --- /dev/null +++ b/dom/script/ScriptLoadContext.cpp @@ -0,0 +1,218 @@ +/* -*- 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 "GeckoProfiler.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "js/OffThreadScriptCompilation.h" +#include "js/SourceText.h" +#include "js/loader/LoadContextBase.h" +#include "js/loader/ModuleLoadRequest.h" + +#include "ScriptLoadContext.h" +#include "ModuleLoadRequest.h" +#include "nsContentUtils.h" +#include "nsICacheInfoChannel.h" +#include "nsIClassOfService.h" +#include "nsISupportsPriority.h" + +namespace mozilla::dom { + +////////////////////////////////////////////////////////////// +// ScriptLoadContext +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptLoadContext) +NS_INTERFACE_MAP_END_INHERITING(JS::loader::LoadContextBase) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ScriptLoadContext) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ScriptLoadContext, + JS::loader::LoadContextBase) + MOZ_ASSERT(!tmp->mOffThreadToken); + MOZ_ASSERT(!tmp->mRunnable); + tmp->MaybeUnblockOnload(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ScriptLoadContext, + JS::loader::LoadContextBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoadBlockedDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(ScriptLoadContext, JS::loader::LoadContextBase) +NS_IMPL_RELEASE_INHERITED(ScriptLoadContext, JS::loader::LoadContextBase) + +ScriptLoadContext::ScriptLoadContext() + : JS::loader::LoadContextBase(JS::loader::ContextKind::Window), + mScriptMode(ScriptMode::eBlocking), + mScriptFromHead(false), + mIsInline(true), + mInDeferList(false), + mInAsyncList(false), + mIsNonAsyncScriptInserted(false), + mIsXSLT(false), + mInCompilingList(false), + mIsTracking(false), + mWasCompiledOMT(false), + mOffThreadToken(nullptr), + mRunnable(nullptr), + mLineNo(1), + mColumnNo(0), + mIsPreload(false), + mUnreportedPreloadError(NS_OK) {} + +ScriptLoadContext::~ScriptLoadContext() { + MOZ_ASSERT(NS_IsMainThread()); + + // Off-thread parsing must have completed by this point. + MOZ_DIAGNOSTIC_ASSERT(!mOffThreadToken && !mRunnable); + + mRequest = nullptr; + + MaybeUnblockOnload(); +} + +void ScriptLoadContext::BlockOnload(Document* aDocument) { + MOZ_ASSERT(!mLoadBlockedDocument); + aDocument->BlockOnload(); + mLoadBlockedDocument = aDocument; +} + +void ScriptLoadContext::MaybeUnblockOnload() { + if (mLoadBlockedDocument) { + mLoadBlockedDocument->UnblockOnload(false); + mLoadBlockedDocument = nullptr; + } +} + +void ScriptLoadContext::MaybeCancelOffThreadScript() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mOffThreadToken) { + return; + } + + // Cancel parse if it hasn't been started yet or wait for it to finish and + // clean up finished parse data. + JSContext* cx = danger::GetJSContext(); + JS::CancelOffThreadToken(cx, mOffThreadToken); + mOffThreadToken = nullptr; + + // Clear the pointer to the runnable. It may still run later if we didn't + // cancel in time. In this case the runnable is held live by the reference + // passed to Dispatch, which is dropped after it runs. + mRunnable = nullptr; + + MaybeUnblockOnload(); +} + +void ScriptLoadContext::SetScriptMode(bool aDeferAttr, bool aAsyncAttr, + bool aLinkPreload) { + if (aLinkPreload) { + mScriptMode = ScriptMode::eLinkPreload; + } else if (aAsyncAttr) { + mScriptMode = ScriptMode::eAsync; + } else if (aDeferAttr || mRequest->IsModuleRequest()) { + mScriptMode = ScriptMode::eDeferred; + } else { + mScriptMode = ScriptMode::eBlocking; + } +} + +// static +void ScriptLoadContext::PrioritizeAsPreload(nsIChannel* aChannel) { + if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } + if (nsCOMPtr<nsISupportsPriority> sp = do_QueryInterface(aChannel)) { + sp->AdjustPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } +} + +void ScriptLoadContext::PrioritizeAsPreload() { + if (!IsLinkPreloadScript()) { + // Do the prioritization only if this request has not already been created + // as a preload. + PrioritizeAsPreload(Channel()); + } +} + +bool ScriptLoadContext::IsPreload() const { + if (mRequest->IsModuleRequest() && !mRequest->IsTopLevel()) { + JS::loader::ModuleLoadRequest* root = + mRequest->AsModuleRequest()->GetRootModule(); + return root->GetScriptLoadContext()->IsPreload(); + } + + MOZ_ASSERT_IF(mIsPreload, !GetScriptElement()); + return mIsPreload; +} + +bool ScriptLoadContext::CompileStarted() const { + return mRequest->IsCompiling() || + (mRequest->IsReadyToRun() && mWasCompiledOMT); +} + +nsIScriptElement* ScriptLoadContext::GetScriptElement() const { + nsCOMPtr<nsIScriptElement> scriptElement = + do_QueryInterface(mRequest->mFetchOptions->mElement); + return scriptElement; +} + +void ScriptLoadContext::SetIsLoadRequest(nsIScriptElement* aElement) { + MOZ_ASSERT(aElement); + MOZ_ASSERT(!GetScriptElement()); + MOZ_ASSERT(IsPreload()); + // We are not tracking our own element, and are relying on the one in + // FetchOptions. + mRequest->mFetchOptions->mElement = do_QueryInterface(aElement); + mIsPreload = false; +} + +void ScriptLoadContext::GetProfilerLabel(nsACString& aOutString) { + if (!profiler_is_active()) { + aOutString.Append("<script> element"); + return; + } + aOutString.Append("<script"); + if (IsAsyncScript()) { + aOutString.Append(" async"); + } else if (IsDeferredScript()) { + aOutString.Append(" defer"); + } + if (mRequest->IsModuleRequest()) { + aOutString.Append(" type=\"module\""); + } + + nsAutoCString url; + if (mRequest->mURI) { + mRequest->mURI->GetAsciiSpec(url); + } else { + url = "<unknown>"; + } + + if (mIsInline) { + if (GetParserCreated() != NOT_FROM_PARSER) { + aOutString.Append("> inline at line "); + aOutString.AppendInt(mLineNo); + aOutString.Append(" of "); + } else { + aOutString.Append("> inline (dynamically created) in "); + } + aOutString.Append(url); + } else { + aOutString.Append(" src=\""); + aOutString.Append(url); + aOutString.Append("\">"); + } +} + +} // namespace mozilla::dom diff --git a/dom/script/ScriptLoadContext.h b/dom/script/ScriptLoadContext.h new file mode 100644 index 0000000000..61aa074667 --- /dev/null +++ b/dom/script/ScriptLoadContext.h @@ -0,0 +1,197 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ScriptLoadContext_h +#define mozilla_dom_ScriptLoadContext_h + +#include "js/AllocPolicy.h" +#include "js/RootingAPI.h" +#include "js/SourceText.h" +#include "js/TypeDecls.h" +#include "js/loader/LoadContextBase.h" +#include "js/loader/ScriptKind.h" +#include "mozilla/Atomics.h" +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/SRIMetadata.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Maybe.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIScriptElement.h" + +class nsICacheInfoChannel; + +namespace JS { +class OffThreadToken; +} // namespace JS + +namespace mozilla::dom { + +class Element; + +/* + * DOM specific ScriptLoadContext. + * + * ScriptLoadContexts augment the loading of a ScriptLoadRequest. They + * describe how a ScriptLoadRequests loading and evaluation needs to be + * augmented, based on the information provided by the loading context. In + * the case of the DOM, the ScriptLoadContext is used to identify how a script + * should be loaded according to information found in the HTML document into + * which it will be loaded. The following fields describe how the + * ScriptLoadRequest will be loaded. + * + * * mScriptMode + * stores the mode (Async, Sync, Deferred), and preload, which + * allows the ScriptLoader to decide if the script should be pushed + * offThread, or if the preloaded request should be used. + * * mScriptFromHead + * Set when the script tag is in the head, and should be treated as + * a blocking script + * * mIsInline + * Set for scripts whose bodies are inline in the html. In this case, + * the script does not need to be fetched first. + * * mIsXSLT + * Set if we are in an XSLT request. + * * mIsPreload + * Set for scripts that are preloaded in a + * <link rel="preload" as="script"> or <link rel="modulepreload"> + * element. + * + * In addition to describing how the ScriptLoadRequest will be loaded by the + * DOM ScriptLoader, the ScriptLoadContext contains fields that facilitate + * those custom behaviors, including support for offthread parsing, pointers + * to runnables (for cancellation and cleanup if a script is parsed offthread) + * and preload element specific controls. + * + */ + +class ScriptLoadContext : public JS::loader::LoadContextBase, + public PreloaderBase { + protected: + virtual ~ScriptLoadContext(); + + public: + explicit ScriptLoadContext(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ScriptLoadContext, + JS::loader::LoadContextBase) + + // PreloaderBase + static void PrioritizeAsPreload(nsIChannel* aChannel); + virtual void PrioritizeAsPreload() override; + + bool IsPreload() const; + + bool CompileStarted() const; + + JS::OffThreadToken** OffThreadTokenPtr() { + return mOffThreadToken ? &mOffThreadToken : nullptr; + } + + bool IsTracking() const { return mIsTracking; } + void SetIsTracking() { + MOZ_ASSERT(!mIsTracking); + mIsTracking = true; + } + + void BlockOnload(Document* aDocument); + + void MaybeUnblockOnload(); + + enum class ScriptMode : uint8_t { + eBlocking, + eDeferred, + eAsync, + eLinkPreload // this is a load initiated by <link rel="preload" + // as="script"> or <link rel="modulepreload"> tag + }; + + void SetScriptMode(bool aDeferAttr, bool aAsyncAttr, bool aLinkPreload); + + bool IsLinkPreloadScript() const { + return mScriptMode == ScriptMode::eLinkPreload; + } + + bool IsBlockingScript() const { return mScriptMode == ScriptMode::eBlocking; } + + bool IsDeferredScript() const { return mScriptMode == ScriptMode::eDeferred; } + + bool IsAsyncScript() const { return mScriptMode == ScriptMode::eAsync; } + + nsIScriptElement* GetScriptElement() const; + + // Make this request a preload (speculative) request. + void SetIsPreloadRequest() { + MOZ_ASSERT(!GetScriptElement()); + MOZ_ASSERT(!IsPreload()); + mIsPreload = true; + } + + // Make a preload request into an actual load request for the given element. + void SetIsLoadRequest(nsIScriptElement* aElement); + + FromParser GetParserCreated() const { + nsIScriptElement* element = GetScriptElement(); + if (!element) { + return NOT_FROM_PARSER; + } + return element->GetParserCreated(); + } + + // Used to output a string for the Gecko Profiler. + void GetProfilerLabel(nsACString& aOutString) override; + + void MaybeCancelOffThreadScript(); + + ScriptMode mScriptMode; // Whether this is a blocking, defer or async script. + bool mScriptFromHead; // Synchronous head script block loading of other non + // js/css content. + bool mIsInline; // Is the script inline or loaded? + bool mInDeferList; // True if we live in mDeferRequests. + bool mInAsyncList; // True if we live in mLoadingAsyncRequests or + // mLoadedAsyncRequests. + bool mIsNonAsyncScriptInserted; // True if we live in + // mNonAsyncExternalScriptInsertedRequests + bool mIsXSLT; // True if we live in mXSLTRequests. + bool mInCompilingList; // True if we are in mOffThreadCompilingRequests. + bool mIsTracking; // True if the script comes from a source on our + // tracking protection list. + bool mWasCompiledOMT; // True if the script has been compiled off main + // thread. + + // Off-thread parsing token. Set at the start of off-thread parsing and + // cleared when the result of the parse is used. + JS::OffThreadToken* mOffThreadToken; + + // Runnable that is dispatched to the main thread when off-thread compilation + // completes. + RefPtr<Runnable> mRunnable; + + uint32_t mLineNo; + uint32_t mColumnNo; + + // Set on scripts and top level modules. + bool mIsPreload; + + // Non-null if there is a document that this request is blocking from loading. + RefPtr<Document> mLoadBlockedDocument; + + // For preload requests, we defer reporting errors to the console until the + // request is used. + nsresult mUnreportedPreloadError; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ScriptLoadContext_h diff --git a/dom/script/ScriptLoadHandler.cpp b/dom/script/ScriptLoadHandler.cpp new file mode 100644 index 0000000000..03ff5301c5 --- /dev/null +++ b/dom/script/ScriptLoadHandler.cpp @@ -0,0 +1,474 @@ +/* -*- 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 "ScriptLoadHandler.h" + +#include <stdlib.h> +#include <utility> +#include "ScriptCompression.h" +#include "ScriptLoader.h" +#include "ScriptTrace.h" +#include "js/Transcoding.h" +#include "js/loader/ScriptLoadRequest.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Encoding.h" +#include "mozilla/Logging.h" +#include "mozilla/NotNull.h" +#include "mozilla/PerfStats.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Utf8.h" +#include "mozilla/Vector.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SRICheck.h" +#include "mozilla/dom/ScriptDecoding.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsICacheInfoChannel.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIRequest.h" +#include "nsIScriptElement.h" +#include "nsIURI.h" +#include "nsJSUtils.h" +#include "nsMimeTypes.h" +#include "nsString.h" +#include "nsTArray.h" +#include "zlib.h" + +namespace mozilla::dom { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) + +ScriptDecoder::ScriptDecoder(const Encoding* aEncoding, + ScriptDecoder::BOMHandling handleBOM) { + if (handleBOM == BOMHandling::Ignore) { + mDecoder = aEncoding->NewDecoderWithoutBOMHandling(); + } else { + mDecoder = aEncoding->NewDecoderWithBOMRemoval(); + } + MOZ_ASSERT(mDecoder); +} + +template <typename Unit> +nsresult ScriptDecoder::DecodeRawDataHelper( + JS::loader::ScriptLoadRequest* aRequest, const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + CheckedInt<size_t> needed = + ScriptDecoding<Unit>::MaxBufferLength(mDecoder, aDataLength); + if (!needed.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Reference to the script source buffer which we will update. + JS::loader::ScriptLoadRequest::ScriptTextBuffer<Unit>& scriptText = + aRequest->ScriptText<Unit>(); + + uint32_t haveRead = scriptText.length(); + + CheckedInt<uint32_t> capacity = haveRead; + capacity += needed.value(); + + if (!capacity.isValid() || !scriptText.resize(capacity.value())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + size_t written = ScriptDecoding<Unit>::DecodeInto( + mDecoder, Span(aData, aDataLength), + Span(scriptText.begin() + haveRead, needed.value()), aEndOfStream); + MOZ_ASSERT(written <= needed.value()); + + haveRead += written; + MOZ_ASSERT(haveRead <= capacity.value(), + "mDecoder produced more data than expected"); + MOZ_ALWAYS_TRUE(scriptText.resize(haveRead)); + aRequest->mScriptTextLength = scriptText.length(); + + return NS_OK; +} + +nsresult ScriptDecoder::DecodeRawData(JS::loader::ScriptLoadRequest* aRequest, + const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + if (aRequest->IsUTF16Text()) { + return DecodeRawDataHelper<char16_t>(aRequest, aData, aDataLength, + aEndOfStream); + } + + return DecodeRawDataHelper<Utf8Unit>(aRequest, aData, aDataLength, + aEndOfStream); +} + +ScriptLoadHandler::ScriptLoadHandler( + ScriptLoader* aScriptLoader, JS::loader::ScriptLoadRequest* aRequest, + UniquePtr<SRICheckDataVerifier>&& aSRIDataVerifier) + : mScriptLoader(aScriptLoader), + mRequest(aRequest), + mSRIDataVerifier(std::move(aSRIDataVerifier)), + mSRIStatus(NS_OK) { + MOZ_ASSERT(aRequest->IsUnknownDataType()); + MOZ_ASSERT(aRequest->IsFetching()); +} + +ScriptLoadHandler::~ScriptLoadHandler() = default; + +NS_IMPL_ISUPPORTS(ScriptLoadHandler, nsIIncrementalStreamLoaderObserver) + +NS_IMETHODIMP +ScriptLoadHandler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + uint32_t aDataLength, const uint8_t* aData, + uint32_t* aConsumedLength) { + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + + auto firstTime = !mPreloadStartNotified; + if (!mPreloadStartNotified) { + mPreloadStartNotified = true; + mRequest->GetScriptLoadContext()->NotifyStart(channelRequest); + } + + if (mRequest->IsCanceled()) { + // If request cancelled, ignore any incoming data. + *aConsumedLength = aDataLength; + return NS_OK; + } + + nsresult rv = NS_OK; + if (mRequest->IsUnknownDataType()) { + rv = EnsureKnownDataType(aLoader); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mRequest->IsBytecode() && firstTime) { + PerfStats::RecordMeasurementStart(PerfStats::Metric::JSBC_IO_Read); + } + + if (mRequest->IsTextSource()) { + if (!EnsureDecoder(aLoader, aData, aDataLength, + /* aEndOfStream = */ false)) { + return NS_OK; + } + + // Below we will/shall consume entire data chunk. + *aConsumedLength = aDataLength; + + // Decoder has already been initialized. -- trying to decode all loaded + // bytes. + rv = mDecoder->DecodeRawData(mRequest, aData, aDataLength, + /* aEndOfStream = */ false); + NS_ENSURE_SUCCESS(rv, rv); + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else { + MOZ_ASSERT(mRequest->IsBytecode()); + if (!mRequest->mScriptBytecode.append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *aConsumedLength = aDataLength; + uint32_t sriLength = 0; + rv = MaybeDecodeSRI(&sriLength); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + if (sriLength) { + mRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + } + } + + return rv; +} + +bool ScriptLoadHandler::TrySetDecoder(nsIIncrementalStreamLoader* aLoader, + const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + MOZ_ASSERT(mDecoder == nullptr, + "can't have a decoder already if we're trying to set one"); + + // JavaScript modules are always UTF-8. + if (mRequest->IsModuleRequest()) { + mDecoder = MakeUnique<ScriptDecoder>(UTF_8_ENCODING, + ScriptDecoder::BOMHandling::Remove); + return true; + } + + // Determine if BOM check should be done. This occurs either + // if end-of-stream has been reached, or at least 3 bytes have + // been read from input. + if (!aEndOfStream && (aDataLength < 3)) { + return false; + } + + // Do BOM detection. + const Encoding* encoding; + std::tie(encoding, std::ignore) = Encoding::ForBOM(Span(aData, aDataLength)); + if (encoding) { + mDecoder = + MakeUnique<ScriptDecoder>(encoding, ScriptDecoder::BOMHandling::Remove); + return true; + } + + // BOM detection failed, check content stream for charset. + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + NS_ASSERTION(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(req); + + if (channel) { + nsAutoCString label; + if (NS_SUCCEEDED(channel->GetContentCharset(label)) && + (encoding = Encoding::ForLabel(label))) { + mDecoder = MakeUnique<ScriptDecoder>(encoding, + ScriptDecoder::BOMHandling::Ignore); + return true; + } + } + + // Check the hint charset from the script element or preload + // request. + nsAutoString hintCharset; + if (!mRequest->GetScriptLoadContext()->IsPreload()) { + mRequest->GetScriptLoadContext()->GetScriptElement()->GetScriptCharset( + hintCharset); + } else { + nsTArray<ScriptLoader::PreloadInfo>::index_type i = + mScriptLoader->mPreloads.IndexOf( + mRequest, 0, ScriptLoader::PreloadRequestComparator()); + + NS_ASSERTION(i != mScriptLoader->mPreloads.NoIndex, + "Incorrect preload bookkeeping"); + hintCharset = mScriptLoader->mPreloads[i].mCharset; + } + + if ((encoding = Encoding::ForLabel(hintCharset))) { + mDecoder = + MakeUnique<ScriptDecoder>(encoding, ScriptDecoder::BOMHandling::Ignore); + return true; + } + + // Get the charset from the charset of the document. + if (mScriptLoader->mDocument) { + encoding = mScriptLoader->mDocument->GetDocumentCharacterSet(); + mDecoder = + MakeUnique<ScriptDecoder>(encoding, ScriptDecoder::BOMHandling::Ignore); + return true; + } + + // Curiously, there are various callers that don't pass aDocument. The + // fallback in the old code was ISO-8859-1, which behaved like + // windows-1252. + mDecoder = MakeUnique<ScriptDecoder>(WINDOWS_1252_ENCODING, + ScriptDecoder::BOMHandling::Ignore); + return true; +} + +nsresult ScriptLoadHandler::MaybeDecodeSRI(uint32_t* sriLength) { + *sriLength = 0; + + if (!mSRIDataVerifier || mSRIDataVerifier->IsComplete() || + NS_FAILED(mSRIStatus)) { + return NS_OK; + } + + // Skip until the content is large enough to be decoded. + if (mRequest->mScriptBytecode.length() <= + mSRIDataVerifier->DataSummaryLength()) { + return NS_OK; + } + + mSRIStatus = mSRIDataVerifier->ImportDataSummary( + mRequest->mScriptBytecode.length(), mRequest->mScriptBytecode.begin()); + + if (NS_FAILED(mSRIStatus)) { + // We are unable to decode the hash contained in the alternate data which + // contains the bytecode, or it does not use the same algorithm. + LOG( + ("ScriptLoadHandler::MaybeDecodeSRI, failed to decode SRI, restart " + "request")); + return mSRIStatus; + } + + *sriLength = mSRIDataVerifier->DataSummaryLength(); + MOZ_ASSERT(*sriLength > 0); + return NS_OK; +} + +nsresult ScriptLoadHandler::EnsureKnownDataType( + nsIIncrementalStreamLoader* aLoader) { + MOZ_ASSERT(mRequest->IsUnknownDataType()); + MOZ_ASSERT(mRequest->IsFetching()); + + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + MOZ_ASSERT(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, rv); + + if (mRequest->mFetchSourceOnly) { + mRequest->SetTextSource(); + TRACE_FOR_TEST(mRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_load_source"); + return NS_OK; + } + + nsCOMPtr<nsICacheInfoChannel> cic(do_QueryInterface(req)); + if (cic) { + nsAutoCString altDataType; + cic->GetAlternativeDataType(altDataType); + if (altDataType.Equals(ScriptLoader::BytecodeMimeTypeFor(mRequest))) { + mRequest->SetBytecode(); + TRACE_FOR_TEST(mRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_load_bytecode"); + return NS_OK; + } + MOZ_ASSERT(altDataType.IsEmpty()); + } + + mRequest->SetTextSource(); + TRACE_FOR_TEST(mRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_load_source"); + + MOZ_ASSERT(!mRequest->IsUnknownDataType()); + MOZ_ASSERT(mRequest->IsFetching()); + return NS_OK; +} + +NS_IMETHODIMP +ScriptLoadHandler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aDataLength, + const uint8_t* aData) { + nsresult rv = NS_OK; + if (LOG_ENABLED()) { + nsAutoCString url; + mRequest->mURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Stream complete (url = %s)", mRequest.get(), + url.get())); + } + + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + + auto firstMessage = !mPreloadStartNotified; + if (!mPreloadStartNotified) { + mPreloadStartNotified = true; + mRequest->GetScriptLoadContext()->NotifyStart(channelRequest); + } + + auto notifyStop = MakeScopeExit([&] { + mRequest->GetScriptLoadContext()->NotifyStop(channelRequest, rv); + }); + + if (!mRequest->IsCanceled()) { + if (mRequest->IsUnknownDataType()) { + rv = EnsureKnownDataType(aLoader); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mRequest->IsBytecode() && !firstMessage) { + // if firstMessage, then entire stream is in aData, and PerfStats would + // measure 0 time + PerfStats::RecordMeasurementEnd(PerfStats::Metric::JSBC_IO_Read); + } + + if (mRequest->IsTextSource()) { + DebugOnly<bool> encoderSet = + EnsureDecoder(aLoader, aData, aDataLength, /* aEndOfStream = */ true); + MOZ_ASSERT(encoderSet); + rv = mDecoder->DecodeRawData(mRequest, aData, aDataLength, + /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("ScriptLoadRequest (%p): Source length in code units = %u", + mRequest.get(), unsigned(mRequest->ScriptTextLength()))); + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else { + MOZ_ASSERT(mRequest->IsBytecode()); + if (!mRequest->mScriptBytecode.append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + LOG(("ScriptLoadRequest (%p): Bytecode length = %u", mRequest.get(), + unsigned(mRequest->mScriptBytecode.length()))); + + // If we abort while decoding the SRI, we fallback on explictly requesting + // the source. Thus, we should not continue in + // ScriptLoader::OnStreamComplete, which removes the request from the + // waiting lists. + // + // We calculate the SRI length below. + uint32_t unused; + rv = MaybeDecodeSRI(&unused); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + + // The bytecode cache always starts with the SRI hash, thus even if there + // is no SRI data verifier instance, we still want to skip the hash. + uint32_t sriLength; + rv = SRICheckDataVerifier::DataSummaryLength( + mRequest->mScriptBytecode.length(), mRequest->mScriptBytecode.begin(), + &sriLength); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + + mRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + + Vector<uint8_t> compressedBytecode; + // mRequest has the compressed bytecode, but will be filled with the + // uncompressed bytecode + compressedBytecode.swap(mRequest->mScriptBytecode); + if (!JS::loader::ScriptBytecodeDecompress(compressedBytecode, + mRequest->mBytecodeOffset, + mRequest->mScriptBytecode)) { + return NS_ERROR_UNEXPECTED; + } + } + } + + // Everything went well, keep the CacheInfoChannel alive such that we can + // later save the bytecode on the cache entry. + if (NS_SUCCEEDED(rv) && mRequest->IsSource() && + StaticPrefs::dom_script_loader_bytecode_cache_enabled()) { + mRequest->mCacheInfo = do_QueryInterface(channelRequest); + LOG(("ScriptLoadRequest (%p): nsICacheInfoChannel = %p", mRequest.get(), + mRequest->mCacheInfo.get())); + } + + // we have to mediate and use mRequest. + rv = mScriptLoader->OnStreamComplete(aLoader, mRequest, aStatus, mSRIStatus, + mSRIDataVerifier.get()); + + // In case of failure, clear the mCacheInfoChannel to avoid keeping it alive. + if (NS_FAILED(rv)) { + mRequest->mCacheInfo = nullptr; + } + + return rv; +} + +#undef LOG_ENABLED +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/script/ScriptLoadHandler.h b/dom/script/ScriptLoadHandler.h new file mode 100644 index 0000000000..8357ad75a8 --- /dev/null +++ b/dom/script/ScriptLoadHandler.h @@ -0,0 +1,136 @@ +/* -*- 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/. */ + +/* + * A class that handles loading and evaluation of <script> elements. + */ + +#ifndef mozilla_dom_ScriptLoadHandler_h +#define mozilla_dom_ScriptLoadHandler_h + +#include "nsIIncrementalStreamLoader.h" +#include "nsISupports.h" +#include "mozilla/Encoding.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace JS::loader { +class ScriptLoadRequest; +} + +namespace mozilla { + +class Decoder; + +namespace dom { + +class ScriptLoader; +class SRICheckDataVerifier; + +class ScriptDecoder { + public: + enum BOMHandling { Ignore, Remove }; + + ScriptDecoder(const Encoding* aEncoding, + ScriptDecoder::BOMHandling handleBOM); + + ~ScriptDecoder() = default; + + /* + * Once the charset is found by the EnsureDecoder function, we can + * incrementally convert the charset to the one expected by the JS Parser. + */ + nsresult DecodeRawData(JS::loader::ScriptLoadRequest* aRequest, + const uint8_t* aData, uint32_t aDataLength, + bool aEndOfStream); + + private: + /* + * Decode the given data into the already-allocated internal + * |ScriptTextBuffer<Unit>|. + * + * This function is intended to be called only by |DecodeRawData| after + * determining which sort of |ScriptTextBuffer<Unit>| has been allocated. + */ + template <typename Unit> + nsresult DecodeRawDataHelper(JS::loader::ScriptLoadRequest* aRequest, + const uint8_t* aData, uint32_t aDataLength, + bool aEndOfStream); + + // Unicode decoder for charset. + mozilla::UniquePtr<mozilla::Decoder> mDecoder; +}; + +class ScriptLoadHandler final : public nsIIncrementalStreamLoaderObserver { + public: + explicit ScriptLoadHandler( + ScriptLoader* aScriptLoader, JS::loader::ScriptLoadRequest* aRequest, + UniquePtr<SRICheckDataVerifier>&& aSRIDataVerifier); + + NS_DECL_ISUPPORTS + NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER + + private: + virtual ~ScriptLoadHandler(); + + /* + * Discover the charset by looking at the stream data, the script tag, and + * other indicators. Returns true if charset has been discovered. + */ + bool EnsureDecoder(nsIIncrementalStreamLoader* aLoader, const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + // Check if the decoder has already been created. + if (mDecoder) { + return true; + } + + return TrySetDecoder(aLoader, aData, aDataLength, aEndOfStream); + } + + /* + * Attempt to determine how script data will be decoded, when such + * determination hasn't already been made. (If you don't know whether it's + * been made yet, use |EnsureDecoder| above instead.) Return false if there + * isn't enough information yet to make the determination, or true if a + * determination was made. + */ + bool TrySetDecoder(nsIIncrementalStreamLoader* aLoader, const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream); + + /* + * When streaming bytecode, we have the opportunity to fallback early if SRI + * does not match the expectation of the document. + * + * If SRI hash is decoded, `sriLength` is set to the length of the hash. + */ + nsresult MaybeDecodeSRI(uint32_t* sriLength); + + // Query the channel to find the data type associated with the input stream. + nsresult EnsureKnownDataType(nsIIncrementalStreamLoader* aLoader); + + // ScriptLoader which will handle the parsed script. + RefPtr<ScriptLoader> mScriptLoader; + + // The ScriptLoadRequest for this load. Decoded data are accumulated on it. + RefPtr<JS::loader::ScriptLoadRequest> mRequest; + + // SRI data verifier. + UniquePtr<SRICheckDataVerifier> mSRIDataVerifier; + + // Status of SRI data operations. + nsresult mSRIStatus; + + UniquePtr<ScriptDecoder> mDecoder; + + // Flipped to true after calling NotifyStart the first time + bool mPreloadStartNotified = false; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptLoadHandler_h diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp new file mode 100644 index 0000000000..219d2d1650 --- /dev/null +++ b/dom/script/ScriptLoader.cpp @@ -0,0 +1,3685 @@ +/* -*- 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 "ScriptLoader.h" +#include "ScriptLoadHandler.h" +#include "ScriptTrace.h" +#include "ModuleLoader.h" + +#include "nsIChildChannel.h" +#include "zlib.h" + +#include "prsystem.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/CompilationAndEvaluation.h" +#include "js/ContextOptions.h" // JS::ContextOptionsRef +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/loader/ScriptLoadRequest.h" +#include "ScriptCompression.h" +#include "js/loader/LoadedScript.h" +#include "js/loader/ModuleLoadRequest.h" +#include "js/MemoryFunctions.h" +#include "js/Modules.h" +#include "js/OffThreadScriptCompilation.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "js/Realm.h" +#include "js/SourceText.h" +#include "js/Transcoding.h" +#include "js/Utility.h" +#include "xpcpublic.h" +#include "GeckoProfiler.h" +#include "nsContentSecurityManager.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIContent.h" +#include "nsJSUtils.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/JSExecutionContext.h" +#include "mozilla/dom/ScriptDecoding.h" // mozilla::dom::ScriptDecoding +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SRILogHelper.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_javascript.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsAboutProtocolUtils.h" +#include "nsGkAtoms.h" +#include "nsNetUtil.h" +#include "nsGlobalWindowInner.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsIPrincipal.h" +#include "nsJSPrincipals.h" +#include "nsContentPolicyUtils.h" +#include "nsIClassifiedChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIClassOfService.h" +#include "nsICacheInfoChannel.h" +#include "nsITimedChannel.h" +#include "nsIScriptElement.h" +#include "nsISupportsPriority.h" +#include "nsIDocShell.h" +#include "nsContentUtils.h" +#include "nsUnicharUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" +#include "nsDocShellCID.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/Logging.h" +#include "nsCRT.h" +#include "nsContentCreatorFunctions.h" +#include "nsProxyRelease.h" +#include "nsSandboxFlags.h" +#include "nsContentTypeParser.h" +#include "nsINetworkPredictor.h" +#include "nsMimeTypes.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/LoadInfo.h" +#include "ReferrerInfo.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "nsIScriptError.h" +#include "nsIAsyncOutputStream.h" + +using JS::SourceText; +using namespace JS::loader; + +using mozilla::Telemetry::LABELS_DOM_SCRIPT_PRELOAD_RESULT; + +namespace mozilla::dom { + +LazyLogModule ScriptLoader::gCspPRLog("CSP"); +LazyLogModule ScriptLoader::gScriptLoaderLog("ScriptLoader"); + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) + +// Alternate Data MIME type used by the ScriptLoader to register that we want to +// store bytecode without reading it. +static constexpr auto kNullMimeType = "javascript/null"_ns; + +///////////////////////////////////////////////////////////// +// AsyncCompileShutdownObserver +///////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(AsyncCompileShutdownObserver, nsIObserver) + +void AsyncCompileShutdownObserver::OnShutdown() { + if (mScriptLoader) { + mScriptLoader->Destroy(); + MOZ_ASSERT(!mScriptLoader); + } +} + +void AsyncCompileShutdownObserver::Unregister() { + if (mScriptLoader) { + mScriptLoader = nullptr; + nsContentUtils::UnregisterShutdownObserver(this); + } +} + +NS_IMETHODIMP +AsyncCompileShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + OnShutdown(); + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// ScriptLoader::PreloadInfo +////////////////////////////////////////////////////////////// + +inline void ImplCycleCollectionUnlink(ScriptLoader::PreloadInfo& aField) { + ImplCycleCollectionUnlink(aField.mRequest); +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoader::PreloadInfo& aField, const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aField.mRequest, aName, aFlags); +} + +////////////////////////////////////////////////////////////// +// ScriptLoader +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptLoader) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ScriptLoader, mNonAsyncExternalScriptInsertedRequests, + mLoadingAsyncRequests, mLoadedAsyncRequests, + mOffThreadCompilingRequests, mDeferRequests, + mXSLTRequests, mParserBlockingRequest, + mBytecodeEncodingQueue, mPreloads, + mPendingChildLoaders, mModuleLoader, + mWebExtModuleLoaders, mShadowRealmModuleLoaders) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptLoader) + +ScriptLoader::ScriptLoader(Document* aDocument) + : mDocument(aDocument), + mParserBlockingBlockerCount(0), + mBlockerCount(0), + mNumberOfProcessors(0), + mTotalFullParseSize(0), + mPhysicalSizeOfMemory(-1), + mEnabled(true), + mDeferEnabled(false), + mSpeculativeOMTParsingEnabled(false), + mDeferCheckpointReached(false), + mBlockingDOMContentLoaded(false), + mLoadEventFired(false), + mGiveUpEncoding(false), + mReporter(new ConsoleReportCollector()) { + LOG(("ScriptLoader::ScriptLoader %p", this)); + + mSpeculativeOMTParsingEnabled = StaticPrefs:: + dom_script_loader_external_scripts_speculative_omt_parse_enabled(); + + mShutdownObserver = new AsyncCompileShutdownObserver(this); + nsContentUtils::RegisterShutdownObserver(mShutdownObserver); +} + +ScriptLoader::~ScriptLoader() { + LOG(("ScriptLoader::~ScriptLoader %p", this)); + + mObservers.Clear(); + + if (mParserBlockingRequest) { + FireScriptAvailable(NS_ERROR_ABORT, mParserBlockingRequest); + } + + for (ScriptLoadRequest* req = mXSLTRequests.getFirst(); req; + req = req->getNext()) { + FireScriptAvailable(NS_ERROR_ABORT, req); + } + + for (ScriptLoadRequest* req = mDeferRequests.getFirst(); req; + req = req->getNext()) { + FireScriptAvailable(NS_ERROR_ABORT, req); + } + + for (ScriptLoadRequest* req = mLoadingAsyncRequests.getFirst(); req; + req = req->getNext()) { + FireScriptAvailable(NS_ERROR_ABORT, req); + } + + for (ScriptLoadRequest* req = mLoadedAsyncRequests.getFirst(); req; + req = req->getNext()) { + FireScriptAvailable(NS_ERROR_ABORT, req); + } + + for (ScriptLoadRequest* req = + mNonAsyncExternalScriptInsertedRequests.getFirst(); + req; req = req->getNext()) { + FireScriptAvailable(NS_ERROR_ABORT, req); + } + + // Unblock the kids, in case any of them moved to a different document + // subtree in the meantime and therefore aren't actually going away. + for (uint32_t j = 0; j < mPendingChildLoaders.Length(); ++j) { + mPendingChildLoaders[j]->RemoveParserBlockingScriptExecutionBlocker(); + } + + for (size_t i = 0; i < mPreloads.Length(); i++) { + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::NotUsed); + } + + if (mShutdownObserver) { + mShutdownObserver->Unregister(); + mShutdownObserver = nullptr; + } + + mModuleLoader = nullptr; +} + +void ScriptLoader::SetGlobalObject(nsIGlobalObject* aGlobalObject) { + if (!aGlobalObject) { + // The document is being detached. + CancelAndClearScriptLoadRequests(); + return; + } + + MOZ_ASSERT(!HasPendingRequests()); + + if (!mModuleLoader) { + // The module loader is associated with a global object, so don't create it + // until we have a global set. + mModuleLoader = new ModuleLoader(this, aGlobalObject, ModuleLoader::Normal); + } + + MOZ_ASSERT(mModuleLoader->GetGlobalObject() == aGlobalObject); + MOZ_ASSERT(aGlobalObject->GetModuleLoader(dom::danger::GetJSContext()) == + mModuleLoader); +} + +void ScriptLoader::RegisterContentScriptModuleLoader(ModuleLoader* aLoader) { + MOZ_ASSERT(aLoader); + MOZ_ASSERT(aLoader->GetScriptLoader() == this); + + mWebExtModuleLoaders.AppendElement(aLoader); +} + +void ScriptLoader::RegisterShadowRealmModuleLoader(ModuleLoader* aLoader) { + MOZ_ASSERT(aLoader); + MOZ_ASSERT(aLoader->GetScriptLoader() == this); + + mShadowRealmModuleLoaders.AppendElement(aLoader); +} + +// Collect telemtry data about the cache information, and the kind of source +// which are being loaded, and where it is being loaded from. +static void CollectScriptTelemetry(ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + + MOZ_ASSERT(aRequest->IsFetching()); + + // Skip this function if we are not running telemetry. + if (!CanRecordExtended()) { + return; + } + + // Report the script kind. + if (aRequest->IsModuleRequest()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_KIND::ModuleScript); + } else { + AccumulateCategorical(LABELS_DOM_SCRIPT_KIND::ClassicScript); + } + + // Report the type of source. This is used to monitor the status of the + // JavaScript Start-up Bytecode Cache, with the expectation of an almost zero + // source-fallback and alternate-data being roughtly equal to source loads. + if (aRequest->mFetchSourceOnly) { + if (aRequest->GetScriptLoadContext()->mIsInline) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::Inline); + } else if (aRequest->IsTextSource()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::SourceFallback); + } + } else { + if (aRequest->IsTextSource()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::Source); + } else if (aRequest->IsBytecode()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::AltData); + } + } +} + +// Helper method for checking if the script element is an event-handler +// This means that it has both a for-attribute and a event-attribute. +// Also, if the for-attribute has a value that matches "\s*window\s*", +// and the event-attribute matches "\s*onload([ \(].*)?" then it isn't an +// eventhandler. (both matches are case insensitive). +// This is how IE seems to filter out a window's onload handler from a +// <script for=... event=...> element. + +static bool IsScriptEventHandler(ScriptKind kind, nsIContent* aScriptElement) { + if (kind != ScriptKind::eClassic) { + return false; + } + + if (!aScriptElement->IsHTMLElement()) { + return false; + } + + nsAutoString forAttr, eventAttr; + if (!aScriptElement->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_for, + forAttr) || + !aScriptElement->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::event, + eventAttr)) { + return false; + } + + const nsAString& for_str = + nsContentUtils::TrimWhitespace<nsCRT::IsAsciiSpace>(forAttr); + if (!for_str.LowerCaseEqualsLiteral("window")) { + return true; + } + + // We found for="window", now check for event="onload". + const nsAString& event_str = + nsContentUtils::TrimWhitespace<nsCRT::IsAsciiSpace>(eventAttr, false); + if (!StringBeginsWith(event_str, u"onload"_ns, + nsCaseInsensitiveStringComparator)) { + // It ain't "onload.*". + + return true; + } + + nsAutoString::const_iterator start, end; + event_str.BeginReading(start); + event_str.EndReading(end); + + start.advance(6); // advance past "onload" + + if (start != end && *start != '(' && *start != ' ') { + // We got onload followed by something other than space or + // '('. Not good enough. + + return true; + } + + return false; +} + +nsContentPolicyType ScriptLoadRequestToContentPolicyType( + ScriptLoadRequest* aRequest) { + if (aRequest->GetScriptLoadContext()->IsPreload()) { + return aRequest->IsModuleRequest() + ? nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD + : nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD; + } + + return aRequest->IsModuleRequest() ? nsIContentPolicy::TYPE_INTERNAL_MODULE + : nsIContentPolicy::TYPE_INTERNAL_SCRIPT; +} + +nsresult ScriptLoader::CheckContentPolicy(Document* aDocument, + nsISupports* aContext, + const nsAString& aType, + ScriptLoadRequest* aRequest) { + nsContentPolicyType contentPolicyType = + ScriptLoadRequestToContentPolicyType(aRequest); + + nsCOMPtr<nsINode> requestingNode = do_QueryInterface(aContext); + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo( + aDocument->NodePrincipal(), // loading principal + aDocument->NodePrincipal(), // triggering principal + requestingNode, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + contentPolicyType); + + // snapshot the nonce at load start time for performing CSP checks + if (contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT || + contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_MODULE) { + nsCOMPtr<nsINode> node = do_QueryInterface(aContext); + if (node) { + nsString* cspNonce = + static_cast<nsString*>(node->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + secCheckLoadInfo->SetCspNonce(*cspNonce); + } + } + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = NS_CheckContentLoadPolicy( + aRequest->mURI, secCheckLoadInfo, NS_LossyConvertUTF16toASCII(aType), + &shouldLoad, nsContentUtils::GetContentPolicy()); + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + if (NS_FAILED(rv) || shouldLoad != nsIContentPolicy::REJECT_TYPE) { + return NS_ERROR_CONTENT_BLOCKED; + } + return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT; + } + + return NS_OK; +} + +/* static */ +bool ScriptLoader::IsAboutPageLoadingChromeURI(ScriptLoadRequest* aRequest, + Document* aDocument) { + // if the uri to be loaded is not of scheme chrome:, there is nothing to do. + if (!aRequest->mURI->SchemeIs("chrome")) { + return false; + } + + // we can either get here with a regular contentPrincipal or with a + // NullPrincipal in case we are showing an error page in a sandboxed iframe. + // In either case if the about: page is linkable from content, there is + // nothing to do. + uint32_t aboutModuleFlags = 0; + nsresult rv = NS_OK; + + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aRequest->TriggeringPrincipal(); + if (triggeringPrincipal->GetIsContentPrincipal()) { + if (!triggeringPrincipal->SchemeIs("about")) { + return false; + } + rv = triggeringPrincipal->GetAboutModuleFlags(&aboutModuleFlags); + NS_ENSURE_SUCCESS(rv, false); + } else if (triggeringPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIURI> docURI = aDocument->GetDocumentURI(); + if (!docURI->SchemeIs("about")) { + return false; + } + + nsCOMPtr<nsIAboutModule> aboutModule; + rv = NS_GetAboutModule(docURI, getter_AddRefs(aboutModule)); + if (NS_FAILED(rv) || !aboutModule) { + return false; + } + rv = aboutModule->GetURIFlags(docURI, &aboutModuleFlags); + NS_ENSURE_SUCCESS(rv, false); + } else { + return false; + } + + if (aboutModuleFlags & nsIAboutModule::MAKE_LINKABLE) { + return false; + } + + // seems like an about page wants to load a chrome URI. + return true; +} + +nsIURI* ScriptLoader::GetBaseURI() const { + MOZ_ASSERT(mDocument); + return mDocument->GetDocBaseURI(); +} + +class ScriptRequestProcessor : public Runnable { + private: + RefPtr<ScriptLoader> mLoader; + RefPtr<ScriptLoadRequest> mRequest; + + public: + ScriptRequestProcessor(ScriptLoader* aLoader, ScriptLoadRequest* aRequest) + : Runnable("dom::ScriptRequestProcessor"), + mLoader(aLoader), + mRequest(aRequest) {} + NS_IMETHOD Run() override { return mLoader->ProcessRequest(mRequest); } +}; + +void ScriptLoader::RunScriptWhenSafe(ScriptLoadRequest* aRequest) { + auto* runnable = new ScriptRequestProcessor(this, aRequest); + nsContentUtils::AddScriptRunner(runnable); +} + +nsresult ScriptLoader::RestartLoad(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsBytecode()); + aRequest->mScriptBytecode.clearAndFree(); + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_fallback"); + + // Notify preload restart so that we can register this preload request again. + aRequest->GetScriptLoadContext()->NotifyRestart(mDocument); + + // Start a new channel from which we explicitly request to stream the source + // instead of the bytecode. + aRequest->mFetchSourceOnly = true; + nsresult rv; + if (aRequest->IsModuleRequest()) { + rv = aRequest->AsModuleRequest()->RestartModuleLoad(); + } else { + rv = StartLoad(aRequest, 0, Nothing()); + } + if (NS_FAILED(rv)) { + return rv; + } + + // Close the current channel and this ScriptLoadHandler as we created a new + // one for the same request. + return NS_BINDING_RETARGETED; +} + +nsresult ScriptLoader::StartLoad( + ScriptLoadRequest* aRequest, uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload) { + if (aRequest->IsModuleRequest()) { + return aRequest->AsModuleRequest()->StartModuleLoad(); + } + + return StartClassicLoad(aRequest, aEarlyHintPreloaderId, aCharsetForPreload); +} + +nsresult ScriptLoader::StartClassicLoad( + ScriptLoadRequest* aRequest, uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload) { + MOZ_ASSERT(aRequest->IsFetching()); + NS_ENSURE_TRUE(mDocument, NS_ERROR_NULL_POINTER); + aRequest->SetUnknownDataType(); + + // If this document is sandboxed without 'allow-scripts', abort. + if (mDocument->HasScriptsBlockedBySandbox()) { + return NS_OK; + } + + if (LOG_ENABLED()) { + nsAutoCString url; + aRequest->mURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Start Classic Load (url = %s)", aRequest, + url.get())); + } + + nsSecurityFlags securityFlags = + nsContentSecurityManager::ComputeSecurityFlags( + aRequest->CORSMode(), nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + nsresult rv = StartLoadInternal(aRequest, securityFlags, + aEarlyHintPreloaderId, aCharsetForPreload); + + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static bool IsWebExtensionRequest(ScriptLoadRequest* aRequest) { + if (!aRequest->IsModuleRequest()) { + return false; + } + + ModuleLoader* loader = + ModuleLoader::From(aRequest->AsModuleRequest()->mLoader); + return loader->GetKind() == ModuleLoader::WebExtension; +} + +nsresult ScriptLoader::StartLoadInternal( + ScriptLoadRequest* aRequest, nsSecurityFlags securityFlags, + uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload) { + nsContentPolicyType contentPolicyType = + ScriptLoadRequestToContentPolicyType(aRequest); + nsCOMPtr<nsINode> context; + if (aRequest->GetScriptLoadContext()->GetScriptElement()) { + context = + do_QueryInterface(aRequest->GetScriptLoadContext()->GetScriptElement()); + } else { + context = mDocument; + } + + nsCOMPtr<nsILoadGroup> loadGroup = mDocument->GetDocumentLoadGroup(); + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + NS_ENSURE_TRUE(window, NS_ERROR_NULL_POINTER); + nsIDocShell* docshell = window->GetDocShell(); + nsCOMPtr<nsIInterfaceRequestor> prompter(do_QueryInterface(docshell)); + + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aRequest->mURI, context, + aRequest->TriggeringPrincipal(), securityFlags, contentPolicyType, + nullptr, // aPerformanceStorage + loadGroup, prompter); + + NS_ENSURE_SUCCESS(rv, rv); + + if (aEarlyHintPreloaderId) { + nsCOMPtr<nsIHttpChannelInternal> channelInternal = + do_QueryInterface(channel); + NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE); + + rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + } + + // snapshot the nonce at load start time for performing CSP checks + if (contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT || + contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_MODULE) { + if (context) { + nsString* cspNonce = + static_cast<nsString*>(context->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + loadInfo->SetCspNonce(*cspNonce); + } + } + } + + nsCOMPtr<nsIScriptGlobalObject> scriptGlobal = GetScriptGlobalObject(); + if (!scriptGlobal) { + return NS_ERROR_FAILURE; + } + + // To avoid decoding issues, the build-id is part of the bytecode MIME type + // constant. + aRequest->mCacheInfo = nullptr; + nsCOMPtr<nsICacheInfoChannel> cic(do_QueryInterface(channel)); + if (cic && StaticPrefs::dom_script_loader_bytecode_cache_enabled()) { + MOZ_ASSERT(!IsWebExtensionRequest(aRequest), + "Can not bytecode cache WebExt code"); + if (!aRequest->mFetchSourceOnly) { + // Inform the HTTP cache that we prefer to have information coming from + // the bytecode cache instead of the sources, if such entry is already + // registered. + LOG(("ScriptLoadRequest (%p): Maybe request bytecode", aRequest)); + cic->PreferAlternativeDataType( + BytecodeMimeTypeFor(aRequest), ""_ns, + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC); + } else { + // If we are explicitly loading from the sources, such as after a + // restarted request, we might still want to save the bytecode after. + // + // The following tell the cache to look for an alternative data type which + // does not exist, such that we can later save the bytecode with a + // different alternative data type. + LOG(("ScriptLoadRequest (%p): Request saving bytecode later", aRequest)); + cic->PreferAlternativeDataType( + kNullMimeType, ""_ns, + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC); + } + } + + LOG(("ScriptLoadRequest (%p): mode=%u tracking=%d", aRequest, + unsigned(aRequest->GetScriptLoadContext()->mScriptMode), + aRequest->GetScriptLoadContext()->IsTracking())); + + if (aRequest->GetScriptLoadContext()->IsLinkPreloadScript()) { + // This is <link rel="preload" as="script"> or <link rel="modulepreload"> + // initiated speculative load, put it to the group that is not blocked by + // leaders and doesn't block follower at the same time. Giving it a much + // higher priority will make this request be processed ahead of other + // Unblocked requests, but with the same weight as Leaders. This will make + // us behave similar way for both http2 and http1. + ScriptLoadContext::PrioritizeAsPreload(channel); + ScriptLoadContext::AddLoadBackgroundFlag(channel); + } else if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(channel)) { + if (aRequest->GetScriptLoadContext()->mScriptFromHead && + aRequest->GetScriptLoadContext()->IsBlockingScript()) { + // synchronous head scripts block loading of most other non js/css + // content such as images, Leader implicitely disallows tailing + cos->AddClassFlags(nsIClassOfService::Leader); + } else if (aRequest->GetScriptLoadContext()->IsDeferredScript() && + !StaticPrefs::network_http_tailing_enabled()) { + // Bug 1395525 and the !StaticPrefs::network_http_tailing_enabled() bit: + // We want to make sure that turing tailing off by the pref makes the + // browser behave exactly the same way as before landing the tailing + // patch. + + // head/body deferred scripts are blocked by leaders but are not + // allowed tailing because they block DOMContentLoaded + cos->AddClassFlags(nsIClassOfService::TailForbidden); + } else { + // other scripts (=body sync or head/body async) are neither blocked + // nor prioritized + cos->AddClassFlags(nsIClassOfService::Unblocked); + + if (aRequest->GetScriptLoadContext()->IsAsyncScript()) { + // async scripts are allowed tailing, since those and only those + // don't block DOMContentLoaded; this flag doesn't enforce tailing, + // just overweights the Unblocked flag when the channel is found + // to be a thrird-party tracker and thus set the Tail flag to engage + // tailing. + cos->AddClassFlags(nsIClassOfService::TailAllowed); + } + } + } + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (httpChannel) { + // HTTP content negotation has little value in this context. + nsAutoCString acceptTypes("*/*"); + rv = httpChannel->SetRequestHeader("Accept"_ns, acceptTypes, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new ReferrerInfo(aRequest->mReferrer, aRequest->ReferrerPolicy()); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIHttpChannelInternal> internalChannel( + do_QueryInterface(httpChannel)); + if (internalChannel) { + rv = internalChannel->SetIntegrityMetadata( + aRequest->mIntegrity.GetIntegrityString()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsAutoString hintCharset; + if (!aRequest->GetScriptLoadContext()->IsPreload() && + aRequest->GetScriptLoadContext()->GetScriptElement()) { + aRequest->GetScriptLoadContext()->GetScriptElement()->GetScriptCharset( + hintCharset); + } else if (aCharsetForPreload.isSome()) { + hintCharset = aCharsetForPreload.ref(); + } + + rv = httpChannel->SetClassicScriptHintCharset(hintCharset); + NS_ENSURE_SUCCESS(rv, rv); + } + + mozilla::net::PredictorLearn( + aRequest->mURI, mDocument->GetDocumentURI(), + nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE, + mDocument->NodePrincipal()->OriginAttributesRef()); + + // Set the initiator type + nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel)); + if (timedChannel) { + if (aEarlyHintPreloaderId) { + timedChannel->SetInitiatorType(u"early-hints"_ns); + } else if (aRequest->GetScriptLoadContext()->IsLinkPreloadScript()) { + timedChannel->SetInitiatorType(u"link"_ns); + } else { + timedChannel->SetInitiatorType(u"script"_ns); + } + } + + UniquePtr<mozilla::dom::SRICheckDataVerifier> sriDataVerifier; + if (!aRequest->mIntegrity.IsEmpty()) { + nsAutoCString sourceUri; + if (mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + sriDataVerifier = MakeUnique<SRICheckDataVerifier>(aRequest->mIntegrity, + sourceUri, mReporter); + } + + RefPtr<ScriptLoadHandler> handler = + new ScriptLoadHandler(this, aRequest, std::move(sriDataVerifier)); + + nsCOMPtr<nsIIncrementalStreamLoader> loader; + rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), handler); + NS_ENSURE_SUCCESS(rv, rv); + + auto key = PreloadHashKey::CreateAsScript( + aRequest->mURI, aRequest->CORSMode(), aRequest->mKind); + aRequest->GetScriptLoadContext()->NotifyOpen( + key, channel, mDocument, + aRequest->GetScriptLoadContext()->IsLinkPreloadScript(), + aRequest->IsModuleRequest()); + + if (aEarlyHintPreloaderId) { + nsCOMPtr<nsIHttpChannelInternal> channelInternal = + do_QueryInterface(channel); + NS_ENSURE_TRUE(channelInternal != nullptr, NS_ERROR_FAILURE); + + rv = channelInternal->SetEarlyHintPreloaderId(aEarlyHintPreloaderId); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = channel->AsyncOpen(loader); + + if (NS_FAILED(rv)) { + // Make sure to inform any <link preload> tags about failure to load the + // resource. + aRequest->GetScriptLoadContext()->NotifyStart(channel); + aRequest->GetScriptLoadContext()->NotifyStop(rv); + } + + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool ScriptLoader::PreloadURIComparator::Equals(const PreloadInfo& aPi, + nsIURI* const& aURI) const { + bool same; + return NS_SUCCEEDED(aPi.mRequest->mURI->Equals(aURI, &same)) && same; +} + +static bool CSPAllowsInlineScript(nsIScriptElement* aElement, + Document* aDocument) { + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + if (!csp) { + // no CSP --> allow + return true; + } + + // query the nonce + nsCOMPtr<Element> scriptContent = do_QueryInterface(aElement); + nsAutoString nonce; + if (scriptContent) { + nsString* cspNonce = + static_cast<nsString*>(scriptContent->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + nonce = *cspNonce; + } + } + + bool parserCreated = + aElement->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER; + + bool allowInlineScript = false; + nsresult rv = csp->GetAllowsInline( + nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE, + false /* aHasUnsafeHash */, nonce, parserCreated, scriptContent, + nullptr /* nsICSPEventListener */, u""_ns, + aElement->GetScriptLineNumber(), aElement->GetScriptColumnNumber(), + &allowInlineScript); + return NS_SUCCEEDED(rv) && allowInlineScript; +} + +already_AddRefed<ScriptLoadRequest> ScriptLoader::CreateLoadRequest( + ScriptKind aKind, nsIURI* aURI, nsIScriptElement* aElement, + nsIPrincipal* aTriggeringPrincipal, CORSMode aCORSMode, + const SRIMetadata& aIntegrity, ReferrerPolicy aReferrerPolicy) { + nsIURI* referrer = mDocument->GetDocumentURIAsReferrer(); + nsCOMPtr<Element> domElement = do_QueryInterface(aElement); + RefPtr<ScriptFetchOptions> fetchOptions = new ScriptFetchOptions( + aCORSMode, aReferrerPolicy, aTriggeringPrincipal, domElement); + RefPtr<ScriptLoadContext> context = new ScriptLoadContext(); + + if (aKind == ScriptKind::eClassic || aKind == ScriptKind::eImportMap) { + RefPtr<ScriptLoadRequest> aRequest = new ScriptLoadRequest( + aKind, aURI, fetchOptions, aIntegrity, referrer, context); + + return aRequest.forget(); + } + + MOZ_ASSERT(aKind == ScriptKind::eModule); + RefPtr<ModuleLoadRequest> aRequest = ModuleLoader::CreateTopLevel( + aURI, fetchOptions, aIntegrity, referrer, this, context); + return aRequest.forget(); +} + +bool ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement, + const nsAutoString& aTypeAttr) { + // We need a document to evaluate scripts. + NS_ENSURE_TRUE(mDocument, false); + + // Check to see if scripts has been turned off. + if (!mEnabled || !mDocument->IsScriptEnabled()) { + return false; + } + + NS_ASSERTION(!aElement->IsMalformed(), "Executing malformed script"); + + nsCOMPtr<nsIContent> scriptContent = do_QueryInterface(aElement); + + ScriptKind scriptKind; + if (aElement->GetScriptIsModule()) { + scriptKind = ScriptKind::eModule; + } else if (aElement->GetScriptIsImportMap()) { + scriptKind = ScriptKind::eImportMap; + } else { + scriptKind = ScriptKind::eClassic; + } + + // Step 13. Check that the script is not an eventhandler + if (IsScriptEventHandler(scriptKind, scriptContent)) { + return false; + } + + // "In modern user agents that support module scripts, the script element with + // the nomodule attribute will be ignored". + // "The nomodule attribute must not be specified on module scripts (and will + // be ignored if it is)." + if (mDocument->ModuleScriptsEnabled() && scriptKind == ScriptKind::eClassic && + scriptContent->IsHTMLElement() && + scriptContent->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::nomodule)) { + return false; + } + + // Step 15. and later in the HTML5 spec + if (aElement->GetScriptExternal()) { + return ProcessExternalScript(aElement, scriptKind, aTypeAttr, + scriptContent); + } + + return ProcessInlineScript(aElement, scriptKind); +} + +bool ScriptLoader::ProcessExternalScript(nsIScriptElement* aElement, + ScriptKind aScriptKind, + const nsAutoString& aTypeAttr, + nsIContent* aScriptContent) { + LOG(("ScriptLoader (%p): Process external script for element %p", this, + aElement)); + + // https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element + // Step 30.1. If el's type is "importmap", then queue an element task on the + // DOM manipulation task source given el to fire an event named error at el, + // and return. + if (aScriptKind == ScriptKind::eImportMap) { + NS_DispatchToCurrentThread( + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent)); + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "Script Loader"_ns, mDocument, + nsContentUtils::eDOM_PROPERTIES, "ImportMapExternalNotSupported"); + return false; + } + + nsCOMPtr<nsIURI> scriptURI = aElement->GetScriptURI(); + if (!scriptURI) { + // Asynchronously report the failure to create a URI object + NS_DispatchToCurrentThread( + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent)); + return false; + } + + SRIMetadata sriMetadata; + { + nsAutoString integrity; + aScriptContent->AsElement()->GetAttr(kNameSpaceID_None, + nsGkAtoms::integrity, integrity); + GetSRIMetadata(integrity, &sriMetadata); + } + + RefPtr<ScriptLoadRequest> request = + LookupPreloadRequest(aElement, aScriptKind, sriMetadata); + + if (request && + NS_FAILED(CheckContentPolicy(mDocument, aElement, aTypeAttr, request))) { + LOG(("ScriptLoader (%p): content policy check failed for preload", this)); + + // Probably plans have changed; even though the preload was allowed seems + // like the actual load is not; let's cancel the preload request. + request->Cancel(); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::RejectedByPolicy); + return false; + } + + if (request) { + // Use the preload request. + + LOG(("ScriptLoadRequest (%p): Using preload request", request.get())); + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-module-script-tree + // Step 1. Disallow further import maps given settings object. + if (request->IsModuleRequest()) { + LOG(("ScriptLoadRequest (%p): Disallow further import maps.", + request.get())); + mModuleLoader->DisallowImportMaps(); + } + + // It's possible these attributes changed since we started the preload so + // update them here. + request->GetScriptLoadContext()->SetScriptMode( + aElement->GetScriptDeferred(), aElement->GetScriptAsync(), false); + + // The request will be added to another list or set as + // mParserBlockingRequest below. + if (request->GetScriptLoadContext()->mInCompilingList) { + mOffThreadCompilingRequests.Remove(request); + request->GetScriptLoadContext()->mInCompilingList = false; + } + + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::Used); + } else { + // No usable preload found. + + nsCOMPtr<nsIPrincipal> principal = + aElement->GetScriptURITriggeringPrincipal(); + if (!principal) { + principal = aScriptContent->NodePrincipal(); + } + + CORSMode ourCORSMode = aElement->GetCORSMode(); + ReferrerPolicy referrerPolicy = GetReferrerPolicy(aElement); + + request = CreateLoadRequest(aScriptKind, scriptURI, aElement, principal, + ourCORSMode, sriMetadata, referrerPolicy); + request->GetScriptLoadContext()->mIsInline = false; + request->GetScriptLoadContext()->SetScriptMode( + aElement->GetScriptDeferred(), aElement->GetScriptAsync(), false); + // keep request->GetScriptLoadContext()->mScriptFromHead to false so we + // don't treat non preloaded scripts as blockers for full page load. See bug + // 792438. + + LOG(("ScriptLoadRequest (%p): Created request for external script", + request.get())); + + nsresult rv = StartLoad(request, 0, Nothing()); + if (NS_FAILED(rv)) { + ReportErrorToConsole(request, rv); + + // Asynchronously report the load failure + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent); + if (mDocument) { + mDocument->Dispatch(TaskCategory::Other, runnable.forget()); + } else { + NS_DispatchToCurrentThread(runnable); + } + return false; + } + } + + // We should still be in loading stage of script unless we're loading a + // module or speculatively off-main-thread parsing a script. + NS_ASSERTION(SpeculativeOMTParsingEnabled() || + !request->GetScriptLoadContext()->CompileStarted() || + request->IsModuleRequest(), + "Request should not yet be in compiling stage."); + + if (request->GetScriptLoadContext()->IsAsyncScript()) { + AddAsyncRequest(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + + // KVKV TODO: Instead of processing immediately, try off-thread-parsing + // it and only schedule a pending ProcessRequest if that fails. + ProcessPendingRequestsAsync(); + } + return false; + } + if (!aElement->GetParserCreated()) { + // Violate the HTML5 spec in order to make LABjs and the "order" plug-in + // for RequireJS work with their Gecko-sniffed code path. See + // http://lists.w3.org/Archives/Public/public-html/2010Oct/0088.html + request->GetScriptLoadContext()->mIsNonAsyncScriptInserted = true; + mNonAsyncExternalScriptInsertedRequests.AppendElement(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + ProcessPendingRequestsAsync(); + } + return false; + } + // we now have a parser-inserted request that may or may not be still + // loading + if (request->GetScriptLoadContext()->IsDeferredScript()) { + // We don't want to run this yet. + // If we come here, the script is a parser-created script and it has + // the defer attribute but not the async attribute. Since a + // a parser-inserted script is being run, we came here by the parser + // running the script, which means the parser is still alive and the + // parse is ongoing. + NS_ASSERTION(mDocument->GetCurrentContentSink() || + aElement->GetParserCreated() == FROM_PARSER_XSLT, + "Non-XSLT Defer script on a document without an active " + "parser; bug 592366."); + AddDeferRequest(request); + return false; + } + + if (aElement->GetParserCreated() == FROM_PARSER_XSLT) { + // Need to maintain order for XSLT-inserted scripts + NS_ASSERTION(!mParserBlockingRequest, + "Parser-blocking scripts and XSLT scripts in the same doc!"); + request->GetScriptLoadContext()->mIsXSLT = true; + mXSLTRequests.AppendElement(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + ProcessPendingRequestsAsync(); + } + return true; + } + + if (request->IsReadyToRun() && ReadyToExecuteParserBlockingScripts()) { + // The request has already been loaded and there are no pending style + // sheets. If the script comes from the network stream, cheat for + // performance reasons and avoid a trip through the event loop. + if (aElement->GetParserCreated() == FROM_PARSER_NETWORK) { + return ProcessRequest(request) == NS_ERROR_HTMLPARSER_BLOCK; + } + // Otherwise, we've got a document.written script, make a trip through + // the event loop to hide the preload effects from the scripts on the + // Web page. + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mParserBlockingRequest = request; + ProcessPendingRequestsAsync(); + return true; + } + + // The script hasn't loaded yet or there's a style sheet blocking it. + // The script will be run when it loads or the style sheet loads. + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mParserBlockingRequest = request; + return true; +} + +bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement, + ScriptKind aScriptKind) { + // Is this document sandboxed without 'allow-scripts'? + if (mDocument->HasScriptsBlockedBySandbox()) { + return false; + } + + // Does CSP allow this inline script to run? + if (!CSPAllowsInlineScript(aElement, mDocument)) { + return false; + } + + // Check if adding an import map script is allowed. If not, we bail out + // early to prevent creating a load request. + if (aScriptKind == ScriptKind::eImportMap) { + // https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element + // Step 31.2 type is "importmap": + // Step 1. If el's relevant global object's import maps allowed is false, + // then queue an element task on the DOM manipulation task source given el + // to fire an event named error at el, and return. + if (!mModuleLoader->IsImportMapAllowed()) { + NS_WARNING("ScriptLoader: import maps allowed is false."); + const char* msg = mModuleLoader->HasImportMapRegistered() + ? "ImportMapNotAllowedMultiple" + : "ImportMapNotAllowedAfterModuleLoad"; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Script Loader"_ns, mDocument, + nsContentUtils::eDOM_PROPERTIES, msg); + NS_DispatchToCurrentThread( + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent)); + return false; + } + } + + // Inline classic scripts ignore their CORS mode and are always CORS_NONE. + CORSMode corsMode = CORS_NONE; + if (aScriptKind == ScriptKind::eModule) { + corsMode = aElement->GetCORSMode(); + } + + ReferrerPolicy referrerPolicy = GetReferrerPolicy(aElement); + RefPtr<ScriptLoadRequest> request = + CreateLoadRequest(aScriptKind, mDocument->GetDocumentURI(), aElement, + mDocument->NodePrincipal(), corsMode, + SRIMetadata(), // SRI doesn't apply + referrerPolicy); + request->GetScriptLoadContext()->mIsInline = true; + request->GetScriptLoadContext()->mLineNo = aElement->GetScriptLineNumber(); + request->GetScriptLoadContext()->mColumnNo = + aElement->GetScriptColumnNumber(); + request->mFetchSourceOnly = true; + request->SetTextSource(); + TRACE_FOR_TEST_BOOL(request->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_load_source"); + CollectScriptTelemetry(request); + + // Only the 'async' attribute is heeded on an inline module script and + // inline classic scripts ignore both these attributes. + MOZ_ASSERT(!aElement->GetScriptDeferred()); + MOZ_ASSERT_IF(!request->IsModuleRequest(), !aElement->GetScriptAsync()); + request->GetScriptLoadContext()->SetScriptMode( + false, aElement->GetScriptAsync(), false); + + LOG(("ScriptLoadRequest (%p): Created request for inline script", + request.get())); + + request->mBaseURL = mDocument->GetDocBaseURI(); + + if (request->IsModuleRequest()) { + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-an-inline-module-script-graph + // Step 1. Disallow further import maps given settings object. + mModuleLoader->DisallowImportMaps(); + + ModuleLoadRequest* modReq = request->AsModuleRequest(); + if (aElement->GetParserCreated() != NOT_FROM_PARSER) { + if (aElement->GetScriptAsync()) { + AddAsyncRequest(modReq); + } else { + AddDeferRequest(modReq); + } + } + + // This calls OnFetchComplete directly since there's no need to start + // fetching an inline script. + nsresult rv = modReq->OnFetchComplete(NS_OK); + if (NS_FAILED(rv)) { + ReportErrorToConsole(modReq, rv); + HandleLoadError(modReq, rv); + } + + return false; + } + + if (request->IsImportMapRequest()) { + // https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element + // Step 31.2 type is "importmap": + // Impl note: Step 1 is done above before creating a ScriptLoadRequest. + MOZ_ASSERT(mModuleLoader->IsImportMapAllowed()); + + // Step 2. Set el's relevant global object's import maps allowed to false. + mModuleLoader->DisallowImportMaps(); + + // Step 3. Let result be the result of creating an import map parse result + // given source text and base URL. + UniquePtr<ImportMap> importMap = mModuleLoader->ParseImportMap(request); + if (!importMap) { + // If parsing import maps fails, the exception will be reported in + // ModuleLoaderBase::ParseImportMap, and the registration of the import + // map will bail out early. + return false; + } + + // TODO: Bug 1781758: Move RegisterImportMap into EvaluateScriptElement. + // + // https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-element + // The spec defines 'register an import map' should be done in + // 'execute the script element', because inside 'execute the script element' + // it will perform a 'preparation-time document check'. + // However, as import maps could be only inline scripts by now, the + // 'preparation-time document check' will never fail for import maps. + // So we simply call 'register an import map' here. + mModuleLoader->RegisterImportMap(std::move(importMap)); + return false; + } + + request->mState = ScriptLoadRequest::State::Ready; + if (aElement->GetParserCreated() == FROM_PARSER_XSLT && + (!ReadyToExecuteParserBlockingScripts() || !mXSLTRequests.isEmpty())) { + // Need to maintain order for XSLT-inserted scripts + NS_ASSERTION(!mParserBlockingRequest, + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mXSLTRequests.AppendElement(request); + return true; + } + if (aElement->GetParserCreated() == NOT_FROM_PARSER) { + NS_ASSERTION( + !nsContentUtils::IsSafeToRunScript(), + "A script-inserted script is inserted without an update batch?"); + RunScriptWhenSafe(request); + return false; + } + if (aElement->GetParserCreated() == FROM_PARSER_NETWORK && + !ReadyToExecuteParserBlockingScripts()) { + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + mParserBlockingRequest = request; + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + return true; + } + // We now have a document.written inline script or we have an inline script + // from the network but there is no style sheet that is blocking scripts. + // Don't check for style sheets blocking scripts in the document.write + // case to avoid style sheet network activity affecting when + // document.write returns. It's not really necessary to do this if + // there's no document.write currently on the call stack. However, + // this way matches IE more closely than checking if document.write + // is on the call stack. + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Not safe to run a parser-inserted script?"); + return ProcessRequest(request) == NS_ERROR_HTMLPARSER_BLOCK; +} + +ScriptLoadRequest* ScriptLoader::LookupPreloadRequest( + nsIScriptElement* aElement, ScriptKind aScriptKind, + const SRIMetadata& aSRIMetadata) { + MOZ_ASSERT(aElement); + + nsTArray<PreloadInfo>::index_type i = + mPreloads.IndexOf(aElement->GetScriptURI(), 0, PreloadURIComparator()); + if (i == nsTArray<PreloadInfo>::NoIndex) { + return nullptr; + } + RefPtr<ScriptLoadRequest> request = mPreloads[i].mRequest; + if (aScriptKind != request->mKind) { + return nullptr; + } + + // Found preloaded request. Note that a script-inserted script can steal a + // preload! + request->GetScriptLoadContext()->SetIsLoadRequest(aElement); + + if (request->GetScriptLoadContext()->mWasCompiledOMT && + !request->IsModuleRequest()) { + request->SetReady(); + } + + nsString preloadCharset(mPreloads[i].mCharset); + mPreloads.RemoveElementAt(i); + + // Double-check that the charset the preload used is the same as the charset + // we have now. + nsAutoString elementCharset; + aElement->GetScriptCharset(elementCharset); + + // Bug 1832361: charset and crossorigin attributes shouldn't affect matching + // of module scripts and modulepreload + if (!request->IsModuleRequest() && + (!elementCharset.Equals(preloadCharset) || + aElement->GetCORSMode() != request->CORSMode())) { + // Drop the preload. + request->Cancel(); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::RequestMismatch); + return nullptr; + } + + if (!aSRIMetadata.CanTrustBeDelegatedTo(request->mIntegrity)) { + // Don't cancel link preload requests, we want to deliver onload according + // the result of the load, cancellation would unexpectedly lead to error + // notification. + if (!request->GetScriptLoadContext()->IsLinkPreloadScript()) { + request->Cancel(); + } + return nullptr; + } + + // Report any errors that we skipped while preloading. + ReportPreloadErrorsToConsole(request); + + // This makes sure the pending preload (if exists) for this resource is + // properly marked as used and thus not notified in the console as unused. + request->GetScriptLoadContext()->NotifyUsage(mDocument); + // A used preload must no longer be found in the Document's hash table. Any + // <link preload> tag after the <script> tag will start a new request, that + // can be satisfied from a different cache, but not from the preload cache. + request->GetScriptLoadContext()->RemoveSelf(mDocument); + + return request; +} + +void ScriptLoader::GetSRIMetadata(const nsAString& aIntegrityAttr, + SRIMetadata* aMetadataOut) { + MOZ_ASSERT(aMetadataOut->IsEmpty()); + + if (aIntegrityAttr.IsEmpty()) { + return; + } + + MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, + ("ScriptLoader::GetSRIMetadata, integrity=%s", + NS_ConvertUTF16toUTF8(aIntegrityAttr).get())); + + nsAutoCString sourceUri; + if (mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + SRICheck::IntegrityMetadata(aIntegrityAttr, sourceUri, mReporter, + aMetadataOut); +} + +ReferrerPolicy ScriptLoader::GetReferrerPolicy(nsIScriptElement* aElement) { + ReferrerPolicy scriptReferrerPolicy = aElement->GetReferrerPolicy(); + if (scriptReferrerPolicy != ReferrerPolicy::_empty) { + return scriptReferrerPolicy; + } + return mDocument->GetReferrerPolicy(); +} + +void ScriptLoader::CancelAndClearScriptLoadRequests() { + // Cancel all requests that have not been executed and remove them. + + if (mParserBlockingRequest) { + mParserBlockingRequest->Cancel(); + mParserBlockingRequest = nullptr; + } + + mDeferRequests.CancelRequestsAndClear(); + mLoadingAsyncRequests.CancelRequestsAndClear(); + mLoadedAsyncRequests.CancelRequestsAndClear(); + mNonAsyncExternalScriptInsertedRequests.CancelRequestsAndClear(); + mXSLTRequests.CancelRequestsAndClear(); + mOffThreadCompilingRequests.CancelRequestsAndClear(); + + if (mModuleLoader) { + mModuleLoader->CancelAndClearDynamicImports(); + } + + for (ModuleLoader* loader : mWebExtModuleLoaders) { + loader->CancelAndClearDynamicImports(); + } + + for (ModuleLoader* loader : mShadowRealmModuleLoaders) { + loader->CancelAndClearDynamicImports(); + } + + for (size_t i = 0; i < mPreloads.Length(); i++) { + mPreloads[i].mRequest->Cancel(); + } + mPreloads.Clear(); +} + +nsresult ScriptLoader::CompileOffThreadOrProcessRequest( + ScriptLoadRequest* aRequest) { + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Processing requests when running scripts is unsafe."); + + if (!aRequest->GetScriptLoadContext()->mOffThreadToken && + !aRequest->GetScriptLoadContext()->CompileStarted()) { + bool couldCompile = false; + nsresult rv = AttemptOffThreadScriptCompile(aRequest, &couldCompile); + if (NS_FAILED(rv)) { + HandleLoadError(aRequest, rv); + return rv; + } + + if (couldCompile) { + return NS_OK; + } + } + + return ProcessRequest(aRequest); +} + +namespace { + +class OffThreadCompilationCompleteRunnable : public Runnable { + nsMainThreadPtrHandle<ScriptLoadRequest> mRequest; + nsMainThreadPtrHandle<ScriptLoader> mLoader; + nsCOMPtr<nsISerialEventTarget> mEventTarget; + JS::OffThreadToken* mToken; + TimeStamp mStartTime; + TimeStamp mStopTime; + + public: + OffThreadCompilationCompleteRunnable(ScriptLoadRequest* aRequest, + ScriptLoader* aLoader) + : Runnable("dom::OffThreadCompilationCompleteRunnable"), + mRequest( + new nsMainThreadPtrHolder<ScriptLoadRequest>("mRequest", aRequest)), + mLoader(new nsMainThreadPtrHolder<ScriptLoader>("mLoader", aLoader)), + mToken(nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + if (DocGroup* docGroup = aLoader->GetDocGroup()) { + mEventTarget = docGroup->EventTargetFor(TaskCategory::Other); + } + } + + void RecordStartTime() { mStartTime = TimeStamp::Now(); } + void RecordStopTime() { mStopTime = TimeStamp::Now(); } + + void SetToken(JS::OffThreadToken* aToken) { + MOZ_ASSERT(aToken && !mToken); + mToken = aToken; + } + + static void Dispatch( + already_AddRefed<OffThreadCompilationCompleteRunnable>&& aSelf) { + RefPtr<OffThreadCompilationCompleteRunnable> self = aSelf; + nsCOMPtr<nsISerialEventTarget> eventTarget = self->mEventTarget; + eventTarget->Dispatch(self.forget()); + } + + NS_DECL_NSIRUNNABLE +}; + +} /* anonymous namespace */ + +nsresult ScriptLoader::AttemptOffThreadScriptCompile( + ScriptLoadRequest* aRequest, bool* aCouldCompileOut) { + // If speculative parsing is enabled, the request may not be ready to run if + // the element is not yet available. + MOZ_ASSERT_IF(!SpeculativeOMTParsingEnabled() && !aRequest->IsModuleRequest(), + aRequest->IsReadyToRun()); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mWasCompiledOMT); + MOZ_ASSERT(aCouldCompileOut && !*aCouldCompileOut); + + // Don't off-thread compile inline scripts. + if (aRequest->GetScriptLoadContext()->mIsInline) { + return NS_OK; + } + + nsCOMPtr<nsIGlobalObject> globalObject = GetGlobalForRequest(aRequest); + if (!globalObject) { + return NS_ERROR_FAILURE; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(globalObject)) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::CompileOptions options(cx); + + // Introduction script will actually be computed and set when the script is + // collected from offthread + JS::Rooted<JSScript*> dummyIntroductionScript(cx); + nsresult rv = FillCompileOptionsForRequest(cx, aRequest, &options, + &dummyIntroductionScript); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aRequest->IsTextSource()) { + if (!JS::CanCompileOffThread(cx, options, aRequest->ScriptTextLength())) { + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_main_thread_compile"); + return NS_OK; + } + } else { + MOZ_ASSERT(aRequest->IsBytecode()); + + size_t length = + aRequest->mScriptBytecode.length() - aRequest->mBytecodeOffset; + JS::DecodeOptions decodeOptions(options); + if (!JS::CanDecodeOffThread(cx, decodeOptions, length)) { + return NS_OK; + } + } + + RefPtr<OffThreadCompilationCompleteRunnable> runnable = + new OffThreadCompilationCompleteRunnable(aRequest, this); + + // Emulate dispatch. CompileOffThreadModule will call + // OffThreadCompilationCompleteCallback were we will emulate run. + LogRunnable::LogDispatch(runnable); + + runnable->RecordStartTime(); + + JS::OffThreadToken* token = nullptr; + rv = StartOffThreadCompilation(cx, aRequest, options, runnable, &token); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(token); + + aRequest->GetScriptLoadContext()->mOffThreadToken = token; + aRequest->GetScriptLoadContext()->mRunnable = runnable; + + aRequest->GetScriptLoadContext()->BlockOnload(mDocument); + + // Once the compilation is finished, a callback will dispatch the runnable to + // the main thread to call ScriptLoader::ProcessOffThreadRequest for the + // request. + aRequest->mState = ScriptLoadRequest::State::Compiling; + + // Requests that are not tracked elsewhere are added to a list while they are + // being compiled off-thread, so we can cancel the compilation later if + // necessary. + // + // Non-top-level modules not tracked because these are cancelled from their + // importing module. + if (aRequest->IsTopLevel() && !aRequest->isInList()) { + mOffThreadCompilingRequests.AppendElement(aRequest); + aRequest->GetScriptLoadContext()->mInCompilingList = true; + } + + *aCouldCompileOut = true; + + return NS_OK; +} + +static inline nsresult CompileResultForToken(void* aToken) { + return aToken ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +nsresult ScriptLoader::StartOffThreadCompilation( + JSContext* aCx, ScriptLoadRequest* aRequest, JS::CompileOptions& aOptions, + Runnable* aRunnable, JS::OffThreadToken** aTokenOut) { + const JS::OffThreadCompileCallback callback = + OffThreadCompilationCompleteCallback; + + if (aRequest->IsBytecode()) { + JS::DecodeOptions decodeOptions(aOptions); + *aTokenOut = JS::DecodeStencilOffThread( + aCx, decodeOptions, aRequest->mScriptBytecode, + aRequest->mBytecodeOffset, callback, aRunnable); + return CompileResultForToken(*aTokenOut); + } + + MaybeSourceText maybeSource; + nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource); + NS_ENSURE_SUCCESS(rv, rv); + + if (aRequest->IsModuleRequest()) { + auto compile = [&](auto& source) { + return JS::CompileModuleToStencilOffThread(aCx, aOptions, source, + callback, aRunnable); + }; + + MOZ_ASSERT(!maybeSource.empty()); + *aTokenOut = maybeSource.mapNonEmpty(compile); + return CompileResultForToken(*aTokenOut); + } + + if (ShouldApplyDelazifyStrategy(aRequest)) { + ApplyDelazifyStrategy(&aOptions); + mTotalFullParseSize += + aRequest->ScriptTextLength() > 0 + ? static_cast<uint32_t>(aRequest->ScriptTextLength()) + : 0; + + LOG( + ("ScriptLoadRequest (%p): non-on-demand-only Parsing Enabled for " + "url=%s mTotalFullParseSize=%u", + aRequest, aRequest->mURI->GetSpecOrDefault().get(), + mTotalFullParseSize)); + } + + if (StaticPrefs::dom_expose_test_interfaces()) { + switch (aOptions.eagerDelazificationStrategy()) { + case JS::DelazificationOption::OnDemandOnly: + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "delazification_on_demand_only"); + break; + case JS::DelazificationOption::CheckConcurrentWithOnDemand: + case JS::DelazificationOption::ConcurrentDepthFirst: + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "delazification_concurrent_depth_first"); + break; + case JS::DelazificationOption::ConcurrentLargeFirst: + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "delazification_concurrent_large_first"); + break; + case JS::DelazificationOption::ParseEverythingEagerly: + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "delazification_parse_everything_eagerly"); + break; + } + } + + auto compile = [&](auto& source) { + return JS::CompileToStencilOffThread(aCx, aOptions, source, callback, + aRunnable); + }; + + MOZ_ASSERT(!maybeSource.empty()); + *aTokenOut = maybeSource.mapNonEmpty(compile); + return CompileResultForToken(*aTokenOut); +} + +void ScriptLoader::OffThreadCompilationCompleteCallback( + JS::OffThreadToken* aToken, void* aCallbackData) { + RefPtr<OffThreadCompilationCompleteRunnable> aRunnable = + static_cast<OffThreadCompilationCompleteRunnable*>(aCallbackData); + + LogRunnable::Run run(aRunnable); + + aRunnable->RecordStopTime(); + aRunnable->SetToken(aToken); + + OffThreadCompilationCompleteRunnable::Dispatch(aRunnable.forget()); +} + +NS_IMETHODIMP +OffThreadCompilationCompleteRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ScriptLoadContext> context = mRequest->GetScriptLoadContext(); + MOZ_ASSERT_IF(context->mRunnable, context->mRunnable == this); + MOZ_ASSERT_IF(context->mOffThreadToken, context->mOffThreadToken == mToken); + + // Clear the pointer to the runnable. The final reference will be released + // when this method returns. + context->mRunnable = nullptr; + + if (!context->mOffThreadToken) { + // Request has been cancelled by MaybeCancelOffThreadScript. + return NS_OK; + } + + if (profiler_is_active()) { + ProfilerString8View scriptSourceString; + if (mRequest->IsTextSource()) { + scriptSourceString = "ScriptCompileOffThread"; + } else { + MOZ_ASSERT(mRequest->IsBytecode()); + scriptSourceString = "BytecodeDecodeOffThread"; + } + + nsAutoCString profilerLabelString; + mRequest->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString); + PROFILER_MARKER_TEXT(scriptSourceString, JS, + MarkerTiming::Interval(mStartTime, mStopTime), + profilerLabelString); + } + + nsresult rv = mLoader->ProcessOffThreadRequest(mRequest); + + mRequest = nullptr; + mLoader = nullptr; + return rv; +} + +nsresult ScriptLoader::ProcessOffThreadRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsCompiling()); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mWasCompiledOMT); + + if (aRequest->IsCanceled()) { + return NS_OK; + } + + aRequest->GetScriptLoadContext()->mWasCompiledOMT = true; + + if (aRequest->GetScriptLoadContext()->mInCompilingList) { + mOffThreadCompilingRequests.Remove(aRequest); + aRequest->GetScriptLoadContext()->mInCompilingList = false; + } + + if (aRequest->IsModuleRequest()) { + MOZ_ASSERT(aRequest->GetScriptLoadContext()->mOffThreadToken); + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + return request->OnFetchComplete(NS_OK); + } + + // Element may not be ready yet if speculatively compiling, so process the + // request in ProcessPendingRequests when it is available. + MOZ_ASSERT_IF(!SpeculativeOMTParsingEnabled(), + aRequest->GetScriptLoadContext()->GetScriptElement()); + if (!aRequest->GetScriptLoadContext()->GetScriptElement()) { + // Unblock onload here in case this request never gets executed. + aRequest->GetScriptLoadContext()->MaybeUnblockOnload(); + return NS_OK; + } + + aRequest->SetReady(); + + if (aRequest == mParserBlockingRequest) { + if (!ReadyToExecuteParserBlockingScripts()) { + // If not ready to execute scripts, schedule an async call to + // ProcessPendingRequests to handle it. + ProcessPendingRequestsAsync(); + return NS_OK; + } + + // Same logic as in top of ProcessPendingRequests. + mParserBlockingRequest = nullptr; + UnblockParser(aRequest); + ProcessRequest(aRequest); + ContinueParserAsync(aRequest); + return NS_OK; + } + + // Async scripts and blocking scripts can be executed right away. + if ((aRequest->GetScriptLoadContext()->IsAsyncScript() || + aRequest->GetScriptLoadContext()->IsBlockingScript()) && + !aRequest->isInList()) { + return ProcessRequest(aRequest); + } + + // Process other scripts in the proper order. + ProcessPendingRequests(); + return NS_OK; +} + +nsresult ScriptLoader::ProcessRequest(ScriptLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Process request", aRequest)); + + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Processing requests when running scripts is unsafe."); + NS_ASSERTION(aRequest->IsReadyToRun(), + "Processing a request that is not ready to run."); + + NS_ENSURE_ARG(aRequest); + + auto unblockOnload = MakeScopeExit( + [&] { aRequest->GetScriptLoadContext()->MaybeUnblockOnload(); }); + + if (aRequest->IsModuleRequest()) { + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + if (request->IsDynamicImport()) { + request->ProcessDynamicImport(); + return NS_OK; + } + + if (request->mModuleScript) { + if (!request->InstantiateModuleGraph()) { + request->mModuleScript = nullptr; + } + } + + if (!request->mModuleScript) { + // There was an error fetching a module script. Nothing to do here. + LOG(("ScriptLoadRequest (%p): Error loading request, firing error", + aRequest)); + FireScriptAvailable(NS_ERROR_FAILURE, aRequest); + return NS_OK; + } + } + + nsCOMPtr<nsINode> scriptElem = + do_QueryInterface(aRequest->GetScriptLoadContext()->GetScriptElement()); + + nsCOMPtr<Document> doc; + if (!aRequest->GetScriptLoadContext()->mIsInline || + aRequest->IsModuleRequest()) { + doc = scriptElem->OwnerDoc(); + } + + nsCOMPtr<nsIScriptElement> oldParserInsertedScript; + uint32_t parserCreated = aRequest->GetScriptLoadContext()->GetParserCreated(); + if (parserCreated) { + oldParserInsertedScript = mCurrentParserInsertedScript; + mCurrentParserInsertedScript = + aRequest->GetScriptLoadContext()->GetScriptElement(); + } + + aRequest->GetScriptLoadContext()->GetScriptElement()->BeginEvaluating(); + + FireScriptAvailable(NS_OK, aRequest); + + // The window may have gone away by this point, in which case there's no point + // in trying to run the script. + + { + // Try to perform a microtask checkpoint + nsAutoMicroTask mt; + } + + nsPIDOMWindowInner* pwin = mDocument->GetInnerWindow(); + bool runScript = !!pwin; + if (runScript) { + nsContentUtils::DispatchTrustedEvent( + scriptElem->OwnerDoc(), scriptElem, u"beforescriptexecute"_ns, + CanBubble::eYes, Cancelable::eYes, &runScript); + } + + // Inner window could have gone away after firing beforescriptexecute + pwin = mDocument->GetInnerWindow(); + if (!pwin) { + runScript = false; + } + + nsresult rv = NS_OK; + if (runScript) { + if (doc) { + doc->IncrementIgnoreDestructiveWritesCounter(); + } + rv = EvaluateScriptElement(aRequest); + if (doc) { + doc->DecrementIgnoreDestructiveWritesCounter(); + } + + nsContentUtils::DispatchTrustedEvent(scriptElem->OwnerDoc(), scriptElem, + u"afterscriptexecute"_ns, + CanBubble::eYes, Cancelable::eNo); + } + + FireScriptEvaluated(rv, aRequest); + + aRequest->GetScriptLoadContext()->GetScriptElement()->EndEvaluating(); + + if (parserCreated) { + mCurrentParserInsertedScript = oldParserInsertedScript; + } + + if (aRequest->GetScriptLoadContext()->mOffThreadToken) { + // The request was parsed off-main-thread, but the result of the off + // thread parse was not actually needed to process the request + // (disappearing window, some other error, ...). Finish the + // request to avoid leaks in the JS engine. + MOZ_ASSERT(!aRequest->IsModuleRequest()); + aRequest->GetScriptLoadContext()->MaybeCancelOffThreadScript(); + } + + // Free any source data, but keep the bytecode content as we might have to + // save it later. + aRequest->ClearScriptSource(); + if (aRequest->IsBytecode()) { + // We received bytecode as input, thus we were decoding, and we will not be + // encoding the bytecode once more. We can safely clear the content of this + // buffer. + aRequest->mScriptBytecode.clearAndFree(); + } + + return rv; +} + +void ScriptLoader::FireScriptAvailable(nsresult aResult, + ScriptLoadRequest* aRequest) { + for (int32_t i = 0; i < mObservers.Count(); i++) { + nsCOMPtr<nsIScriptLoaderObserver> obs = mObservers[i]; + obs->ScriptAvailable( + aResult, aRequest->GetScriptLoadContext()->GetScriptElement(), + aRequest->GetScriptLoadContext()->mIsInline, aRequest->mURI, + aRequest->GetScriptLoadContext()->mLineNo); + } + + bool isInlineClassicScript = aRequest->GetScriptLoadContext()->mIsInline && + !aRequest->IsModuleRequest(); + RefPtr<nsIScriptElement> scriptElement = + aRequest->GetScriptLoadContext()->GetScriptElement(); + scriptElement->ScriptAvailable(aResult, scriptElement, isInlineClassicScript, + aRequest->mURI, + aRequest->GetScriptLoadContext()->mLineNo); +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) +MOZ_CAN_RUN_SCRIPT_BOUNDARY void ScriptLoader::FireScriptEvaluated( + nsresult aResult, ScriptLoadRequest* aRequest) { + for (int32_t i = 0; i < mObservers.Count(); i++) { + nsCOMPtr<nsIScriptLoaderObserver> obs = mObservers[i]; + RefPtr<nsIScriptElement> scriptElement = + aRequest->GetScriptLoadContext()->GetScriptElement(); + obs->ScriptEvaluated(aResult, scriptElement, + aRequest->GetScriptLoadContext()->mIsInline); + } + + RefPtr<nsIScriptElement> scriptElement = + aRequest->GetScriptLoadContext()->GetScriptElement(); + scriptElement->ScriptEvaluated(aResult, scriptElement, + aRequest->GetScriptLoadContext()->mIsInline); +} + +already_AddRefed<nsIGlobalObject> ScriptLoader::GetGlobalForRequest( + ScriptLoadRequest* aRequest) { + if (aRequest->IsModuleRequest()) { + ModuleLoader* loader = + ModuleLoader::From(aRequest->AsModuleRequest()->mLoader); + nsCOMPtr<nsIGlobalObject> global = loader->GetGlobalObject(); + return global.forget(); + } + + return GetScriptGlobalObject(); +} + +already_AddRefed<nsIScriptGlobalObject> ScriptLoader::GetScriptGlobalObject() { + if (!mDocument) { + return nullptr; + } + + nsPIDOMWindowInner* pwin = mDocument->GetInnerWindow(); + if (!pwin) { + return nullptr; + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = do_QueryInterface(pwin); + NS_ASSERTION(globalObject, "windows must be global objects"); + + // and make sure we are setup for this type of script. + nsresult rv = globalObject->EnsureScriptEnvironment(); + if (NS_FAILED(rv)) { + return nullptr; + } + + return globalObject.forget(); +} + +nsresult ScriptLoader::FillCompileOptionsForRequest( + JSContext* aCx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) { + // It's very important to use aRequest->mURI, not the final URI of the channel + // aRequest ended up getting script data from, as the script filename. + nsresult rv = aRequest->mURI->GetSpec(aRequest->mURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mDocument) { + mDocument->NoteScriptTrackingStatus( + aRequest->mURL, aRequest->GetScriptLoadContext()->IsTracking()); + } + + const char* introductionType; + if (aRequest->IsModuleRequest() && + !aRequest->AsModuleRequest()->IsTopLevel()) { + introductionType = "importedModule"; + } else if (!aRequest->GetScriptLoadContext()->mIsInline) { + introductionType = "srcScript"; + } else if (aRequest->GetScriptLoadContext()->GetParserCreated() == + FROM_PARSER_NETWORK) { + introductionType = "inlineScript"; + } else { + introductionType = "injectedScript"; + } + aOptions->setIntroductionInfoToCaller(aCx, introductionType, + aIntroductionScript); + aOptions->setFileAndLine(aRequest->mURL.get(), + aRequest->GetScriptLoadContext()->mLineNo); + // The column is only relevant for inline scripts in order for SpiderMonkey to + // properly compute offsets relatively to the script position within the HTML + // file. injectedScript are not concerned and are always considered to start + // at column 0. + if (aRequest->GetScriptLoadContext()->mIsInline && + aRequest->GetScriptLoadContext()->GetParserCreated() == + FROM_PARSER_NETWORK) { + aOptions->setColumn(aRequest->GetScriptLoadContext()->mColumnNo); + } + aOptions->setIsRunOnce(true); + aOptions->setNoScriptRval(true); + if (aRequest->mSourceMapURL) { + aOptions->setSourceMapURL(aRequest->mSourceMapURL->get()); + } + if (aRequest->mOriginPrincipal) { + nsCOMPtr<nsIGlobalObject> globalObject = GetGlobalForRequest(aRequest); + nsIPrincipal* scriptPrin = globalObject->PrincipalOrNull(); + MOZ_ASSERT(scriptPrin); + bool subsumes = scriptPrin->Subsumes(aRequest->mOriginPrincipal); + aOptions->setMutedErrors(!subsumes); + } + + if (aRequest->IsModuleRequest()) { + aOptions->setHideScriptFromDebugger(true); + } + + aOptions->setDeferDebugMetadata(true); + + aOptions->borrowBuffer = true; + + aOptions->allocateInstantiationStorage = true; + + return NS_OK; +} + +/* static */ +bool ScriptLoader::ShouldCacheBytecode(ScriptLoadRequest* aRequest) { + using mozilla::TimeDuration; + using mozilla::TimeStamp; + + // We need the nsICacheInfoChannel to exist to be able to open the alternate + // data output stream. This pointer would only be non-null if the bytecode was + // activated at the time the channel got created in StartLoad. + if (!aRequest->mCacheInfo) { + LOG(("ScriptLoadRequest (%p): Cannot cache anything (cacheInfo = %p)", + aRequest, aRequest->mCacheInfo.get())); + return false; + } + + // Look at the preference to know which strategy (parameters) should be used + // when the bytecode cache is enabled. + int32_t strategy = StaticPrefs::dom_script_loader_bytecode_cache_strategy(); + + // List of parameters used by the strategies. + bool hasSourceLengthMin = false; + bool hasFetchCountMin = false; + size_t sourceLengthMin = 100; + uint32_t fetchCountMin = 4; + + LOG(("ScriptLoadRequest (%p): Bytecode-cache: strategy = %d.", aRequest, + strategy)); + switch (strategy) { + case -2: { + // Reader mode, keep requesting alternate data but no longer save it. + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Encoding disabled.", + aRequest)); + return false; + } + case -1: { + // Eager mode, skip heuristics! + hasSourceLengthMin = false; + hasFetchCountMin = false; + break; + } + default: + case 0: { + hasSourceLengthMin = true; + hasFetchCountMin = true; + sourceLengthMin = 1024; + // If we were to optimize only for speed, without considering the impact + // on memory, we should set this threshold to 2. (Bug 900784 comment 120) + fetchCountMin = 4; + break; + } + } + + // If the script is too small/large, do not attempt at creating a bytecode + // cache for this script, as the overhead of parsing it might not be worth the + // effort. + if (hasSourceLengthMin) { + size_t sourceLength; + size_t minLength; + MOZ_ASSERT(aRequest->IsTextSource()); + sourceLength = aRequest->mScriptTextLength; + minLength = sourceLengthMin; + if (sourceLength < minLength) { + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Script is too small.", + aRequest)); + return false; + } + } + + // Check that we loaded the cache entry a few times before attempting any + // bytecode-cache optimization, such that we do not waste time on entry which + // are going to be dropped soon. + if (hasFetchCountMin) { + uint32_t fetchCount = 0; + if (NS_FAILED(aRequest->mCacheInfo->GetCacheTokenFetchCount(&fetchCount))) { + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Cannot get fetchCount.", + aRequest)); + return false; + } + LOG(("ScriptLoadRequest (%p): Bytecode-cache: fetchCount = %d.", aRequest, + fetchCount)); + if (fetchCount < fetchCountMin) { + return false; + } + } + + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Trigger encoding.", aRequest)); + return true; +} + +class MOZ_RAII AutoSetProcessingScriptTag { + nsCOMPtr<nsIScriptContext> mContext; + bool mOldTag; + + public: + explicit AutoSetProcessingScriptTag(nsIScriptContext* aContext) + : mContext(aContext), mOldTag(mContext->GetProcessingScriptTag()) { + mContext->SetProcessingScriptTag(true); + } + + ~AutoSetProcessingScriptTag() { mContext->SetProcessingScriptTag(mOldTag); } +}; + +static nsresult ExecuteCompiledScript(JSContext* aCx, JSExecutionContext& aExec, + ClassicScript* aLoaderScript) { + JS::Rooted<JSScript*> script(aCx, aExec.GetScript()); + if (!script) { + // Compilation succeeds without producing a script if scripting is + // disabled for the global. + return NS_OK; + } + + if (JS::GetScriptPrivate(script).isUndefined()) { + aLoaderScript->AssociateWithScript(script); + } + + return aExec.ExecScript(); +} + +nsresult ScriptLoader::EvaluateScriptElement(ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + MOZ_ASSERT(aRequest->IsReadyToRun()); + + // We need a document to evaluate scripts. + if (!mDocument) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIContent> scriptContent( + do_QueryInterface(aRequest->GetScriptLoadContext()->GetScriptElement())); + MOZ_ASSERT(scriptContent); + Document* ownerDoc = scriptContent->OwnerDoc(); + if (ownerDoc != mDocument) { + // Willful violation of HTML5 as of 2010-12-01 + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIGlobalObject> globalObject; + nsCOMPtr<nsIScriptContext> context; + if (!IsWebExtensionRequest(aRequest)) { + // Otherwise we have to ensure that there is a nsIScriptContext. + nsCOMPtr<nsIScriptGlobalObject> scriptGlobal = GetScriptGlobalObject(); + if (!scriptGlobal) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT_IF( + aRequest->IsModuleRequest(), + aRequest->AsModuleRequest()->GetGlobalObject() == scriptGlobal); + + // Make sure context is a strong reference since we access it after + // we've executed a script, which may cause all other references to + // the context to go away. + context = scriptGlobal->GetScriptContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + + globalObject = scriptGlobal; + } + + // Update our current script. + // This must be destroyed after destroying nsAutoMicroTask, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1620505#c4 + nsIScriptElement* currentScript = + aRequest->IsModuleRequest() + ? nullptr + : aRequest->GetScriptLoadContext()->GetScriptElement(); + AutoCurrentScriptUpdater scriptUpdater(this, currentScript); + + Maybe<AutoSetProcessingScriptTag> setProcessingScriptTag; + if (context) { + setProcessingScriptTag.emplace(context); + } + + // https://wicg.github.io/import-maps/#integration-script-type + // Switch on the script's type for scriptElement: + // "importmap" + // Assert: Never reached. + MOZ_ASSERT(!aRequest->IsImportMapRequest()); + + if (aRequest->IsModuleRequest()) { + return aRequest->AsModuleRequest()->EvaluateModule(); + } + + return EvaluateScript(globalObject, aRequest); +} + +nsresult ScriptLoader::CompileOrDecodeClassicScript( + JSContext* aCx, JSExecutionContext& aExec, ScriptLoadRequest* aRequest) { + nsAutoCString profilerLabelString; + aRequest->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString); + + nsresult rv; + if (aRequest->IsBytecode()) { + if (aRequest->GetScriptLoadContext()->mOffThreadToken) { + LOG(("ScriptLoadRequest (%p): Decode Bytecode & Join and Execute", + aRequest)); + rv = aExec.JoinOffThread( + &aRequest->GetScriptLoadContext()->mOffThreadToken); + } else { + LOG(("ScriptLoadRequest (%p): Decode Bytecode and Execute", aRequest)); + AUTO_PROFILER_MARKER_TEXT("BytecodeDecodeMainThread", JS, + MarkerInnerWindowIdFromJSContext(aCx), + profilerLabelString); + + rv = aExec.Decode(aRequest->mScriptBytecode, aRequest->mBytecodeOffset); + } + + // We do not expect to be saving anything when we already have some + // bytecode. + MOZ_ASSERT(!aRequest->mCacheInfo); + return rv; + } + + MOZ_ASSERT(aRequest->IsSource()); + bool encodeBytecode = ShouldCacheBytecode(aRequest); + aExec.SetEncodeBytecode(encodeBytecode); + + if (aRequest->GetScriptLoadContext()->mOffThreadToken) { + // Off-main-thread parsing. + LOG( + ("ScriptLoadRequest (%p): Join (off-thread parsing) and " + "Execute", + aRequest)); + MOZ_ASSERT(aRequest->IsTextSource()); + rv = + aExec.JoinOffThread(&aRequest->GetScriptLoadContext()->mOffThreadToken); + } else { + // Main thread parsing (inline and small scripts) + LOG(("ScriptLoadRequest (%p): Compile And Exec", aRequest)); + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + rv = aRequest->GetScriptSource(aCx, &maybeSource); + if (NS_SUCCEEDED(rv)) { + AUTO_PROFILER_MARKER_TEXT("ScriptCompileMainThread", JS, + MarkerInnerWindowIdFromJSContext(aCx), + profilerLabelString); + + auto compile = [&](auto& source) { return aExec.Compile(source); }; + + MOZ_ASSERT(!maybeSource.empty()); + TimeStamp startTime = TimeStamp::Now(); + rv = maybeSource.mapNonEmpty(compile); + mMainThreadParseTime += TimeStamp::Now() - startTime; + } + } + return rv; +} + +/* static */ +nsCString& ScriptLoader::BytecodeMimeTypeFor(ScriptLoadRequest* aRequest) { + if (aRequest->IsModuleRequest()) { + return nsContentUtils::JSModuleBytecodeMimeType(); + } + return nsContentUtils::JSScriptBytecodeMimeType(); +} + +void ScriptLoader::MaybePrepareForBytecodeEncodingBeforeExecute( + ScriptLoadRequest* aRequest, JS::Handle<JSScript*> aScript) { + if (!ShouldCacheBytecode(aRequest)) { + return; + } + + aRequest->MarkForBytecodeEncoding(aScript); +} + +nsresult ScriptLoader::MaybePrepareForBytecodeEncodingAfterExecute( + ScriptLoadRequest* aRequest, nsresult aRv) { + if (aRequest->IsMarkedForBytecodeEncoding()) { + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_encode"); + // NOTE: This assertion will fail once we start encoding more data after the + // first encode. + MOZ_ASSERT(aRequest->mBytecodeOffset == aRequest->mScriptBytecode.length()); + RegisterForBytecodeEncoding(aRequest); + MOZ_ASSERT(IsAlreadyHandledForBytecodeEncodingPreparation(aRequest)); + + return aRv; + } + + LOG(("ScriptLoadRequest (%p): Bytecode-cache: disabled (rv = %X)", aRequest, + unsigned(aRv))); + TRACE_FOR_TEST_NONE(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_no_encode"); + aRequest->mCacheInfo = nullptr; + MOZ_ASSERT(IsAlreadyHandledForBytecodeEncodingPreparation(aRequest)); + + return aRv; +} + +bool ScriptLoader::IsAlreadyHandledForBytecodeEncodingPreparation( + ScriptLoadRequest* aRequest) { + return aRequest->isInList() || !aRequest->mCacheInfo; +} + +void ScriptLoader::MaybePrepareModuleForBytecodeEncodingBeforeExecute( + JSContext* aCx, ModuleLoadRequest* aRequest) { + { + ModuleScript* moduleScript = aRequest->mModuleScript; + JS::Rooted<JSObject*> module(aCx, moduleScript->ModuleRecord()); + + if (aRequest->IsMarkedForBytecodeEncoding()) { + // This module is imported multiple times, and already marked. + return; + } + + if (ShouldCacheBytecode(aRequest)) { + aRequest->MarkModuleForBytecodeEncoding(); + } + } + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + MaybePrepareModuleForBytecodeEncodingBeforeExecute(aCx, childRequest); + } +} + +nsresult ScriptLoader::MaybePrepareModuleForBytecodeEncodingAfterExecute( + ModuleLoadRequest* aRequest, nsresult aRv) { + if (IsAlreadyHandledForBytecodeEncodingPreparation(aRequest)) { + // This module is imported multiple times and already handled. + return aRv; + } + + aRv = MaybePrepareForBytecodeEncodingAfterExecute(aRequest, aRv); + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + aRv = MaybePrepareModuleForBytecodeEncodingAfterExecute(childRequest, aRv); + } + + return aRv; +} + +nsresult ScriptLoader::EvaluateScript(nsIGlobalObject* aGlobalObject, + ScriptLoadRequest* aRequest) { + nsAutoMicroTask mt; + AutoEntryScript aes(aGlobalObject, "EvaluateScript", true); + JSContext* cx = aes.cx(); + + nsAutoCString profilerLabelString; + aRequest->GetScriptLoadContext()->GetProfilerLabel(profilerLabelString); + + // Create a ClassicScript object and associate it with the JSScript. + RefPtr<ClassicScript> classicScript = + new ClassicScript(aRequest->mFetchOptions, aRequest->mBaseURL); + JS::Rooted<JS::Value> classicScriptValue(cx, JS::PrivateValue(classicScript)); + + JS::CompileOptions options(cx); + JS::Rooted<JSScript*> introductionScript(cx); + nsresult rv = + FillCompileOptionsForRequest(cx, aRequest, &options, &introductionScript); + + if (NS_FAILED(rv)) { + return rv; + } + + TRACE_FOR_TEST(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_execute"); + JS::Rooted<JSObject*> global(cx, aGlobalObject->GetGlobalJSObject()); + JSExecutionContext exec(cx, global, options, classicScriptValue, + introductionScript); + + rv = CompileOrDecodeClassicScript(cx, exec, aRequest); + + if (NS_FAILED(rv)) { + return rv; + } + + // TODO (yulia): rewrite this section. rv can be a failing pattern other than + // NS_OK which will pass the NS_FAILED check above. If we call exec.GetScript + // in that case, it will crash. + if (rv == NS_OK) { + JS::Rooted<JSScript*> script(cx, exec.GetScript()); + MaybePrepareForBytecodeEncodingBeforeExecute(aRequest, script); + + { + LOG(("ScriptLoadRequest (%p): Evaluate Script", aRequest)); + AUTO_PROFILER_MARKER_TEXT("ScriptExecution", JS, + MarkerInnerWindowIdFromJSContext(cx), + profilerLabelString); + + rv = ExecuteCompiledScript(cx, exec, classicScript); + } + } + + // This must be called also for compilation failure case, in order to + // dispatch test-only event. + rv = MaybePrepareForBytecodeEncodingAfterExecute(aRequest, rv); + + // Even if we are not saving the bytecode of the current script, we have + // to trigger the encoding of the bytecode, as the current script can + // call functions of a script for which we are recording the bytecode. + LOG(("ScriptLoadRequest (%p): ScriptLoader = %p", aRequest, this)); + MaybeTriggerBytecodeEncoding(); + + return rv; +} + +/* static */ +LoadedScript* ScriptLoader::GetActiveScript(JSContext* aCx) { + JS::Value value = JS::GetScriptedCallerPrivate(aCx); + if (value.isUndefined()) { + return nullptr; + } + + return static_cast<LoadedScript*>(value.toPrivate()); +} + +void ScriptLoader::RegisterForBytecodeEncoding(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mCacheInfo); + MOZ_ASSERT(aRequest->IsMarkedForBytecodeEncoding()); + MOZ_DIAGNOSTIC_ASSERT(!aRequest->isInList()); + mBytecodeEncodingQueue.AppendElement(aRequest); +} + +void ScriptLoader::LoadEventFired() { + mLoadEventFired = true; + MaybeTriggerBytecodeEncoding(); + + if (!mMainThreadParseTime.IsZero()) { + Telemetry::Accumulate( + Telemetry::JS_PAGELOAD_PARSE_MS, + static_cast<uint32_t>(mMainThreadParseTime.ToMilliseconds())); + } +} + +void ScriptLoader::Destroy() { + if (mShutdownObserver) { + mShutdownObserver->Unregister(); + mShutdownObserver = nullptr; + } + + CancelAndClearScriptLoadRequests(); + GiveUpBytecodeEncoding(); +} + +void ScriptLoader::MaybeTriggerBytecodeEncoding() { + // If we already gave up, ensure that we are not going to enqueue any script, + // and that we finalize them properly. + if (mGiveUpEncoding) { + LOG(("ScriptLoader (%p): Keep giving-up bytecode encoding.", this)); + GiveUpBytecodeEncoding(); + return; + } + + // We wait for the load event to be fired before saving the bytecode of + // any script to the cache. It is quite common to have load event + // listeners trigger more JavaScript execution, that we want to save as + // part of this start-up bytecode cache. + if (!mLoadEventFired) { + LOG(("ScriptLoader (%p): Wait for the load-end event to fire.", this)); + return; + } + + // No need to fire any event if there is no bytecode to be saved. + if (mBytecodeEncodingQueue.isEmpty()) { + LOG(("ScriptLoader (%p): No script in queue to be encoded.", this)); + return; + } + + // Wait until all scripts are loaded before saving the bytecode, such that + // we capture most of the intialization of the page. + if (HasPendingRequests()) { + LOG(("ScriptLoader (%p): Wait for other pending request to finish.", this)); + return; + } + + // Create a new runnable dedicated to encoding the content of the bytecode of + // all enqueued scripts when the document is idle. In case of failure, we + // give-up on encoding the bytecode. + nsCOMPtr<nsIRunnable> encoder = NewRunnableMethod( + "ScriptLoader::EncodeBytecode", this, &ScriptLoader::EncodeBytecode); + if (NS_FAILED(NS_DispatchToCurrentThreadQueue(encoder.forget(), + EventQueuePriority::Idle))) { + GiveUpBytecodeEncoding(); + return; + } + + LOG(("ScriptLoader (%p): Schedule bytecode encoding.", this)); +} + +void ScriptLoader::EncodeBytecode() { + LOG(("ScriptLoader (%p): Start bytecode encoding.", this)); + + // If any script got added in the previous loop cycle, wait until all + // remaining script executions are completed, such that we capture most of + // the initialization. + if (HasPendingRequests()) { + return; + } + + // Should not be encoding modules at all. + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + GiveUpBytecodeEncoding(); + return; + } + + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (!context) { + GiveUpBytecodeEncoding(); + return; + } + + AutoEntryScript aes(globalObject, "encode bytecode", true); + RefPtr<ScriptLoadRequest> request; + while (!mBytecodeEncodingQueue.isEmpty()) { + request = mBytecodeEncodingQueue.StealFirst(); + MOZ_ASSERT(!IsWebExtensionRequest(request), + "Bytecode for web extension content scrips is not cached"); + EncodeRequestBytecode(aes.cx(), request); + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } +} + +void ScriptLoader::EncodeRequestBytecode(JSContext* aCx, + ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + nsresult rv = NS_OK; + MOZ_ASSERT(aRequest->mCacheInfo); + auto bytecodeFailed = mozilla::MakeScopeExit([&]() { + TRACE_FOR_TEST_NONE(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_bytecode_failed"); + }); + + bool result; + if (aRequest->IsModuleRequest()) { + ModuleScript* moduleScript = aRequest->AsModuleRequest()->mModuleScript; + JS::Rooted<JSObject*> module(aCx, moduleScript->ModuleRecord()); + result = + JS::FinishIncrementalEncoding(aCx, module, aRequest->mScriptBytecode); + } else { + JS::Rooted<JSScript*> script(aCx, aRequest->mScriptForBytecodeEncoding); + result = + JS::FinishIncrementalEncoding(aCx, script, aRequest->mScriptBytecode); + } + if (!result) { + // Encoding can be aborted for non-supported syntax (e.g. asm.js), or + // any other internal error. + // We don't care the error and just give up encoding. + JS_ClearPendingException(aCx); + + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", aRequest)); + return; + } + + Vector<uint8_t> compressedBytecode; + // TODO probably need to move this to a helper thread + if (!ScriptBytecodeCompress(aRequest->mScriptBytecode, + aRequest->mBytecodeOffset, compressedBytecode)) { + return; + } + + if (compressedBytecode.length() >= UINT32_MAX) { + LOG( + ("ScriptLoadRequest (%p): Bytecode cache is too large to be decoded " + "correctly.", + aRequest)); + return; + } + + // Open the output stream to the cache entry alternate data storage. This + // might fail if the stream is already open by another request, in which + // case, we just ignore the current one. + nsCOMPtr<nsIAsyncOutputStream> output; + rv = aRequest->mCacheInfo->OpenAlternativeOutputStream( + BytecodeMimeTypeFor(aRequest), + static_cast<int64_t>(compressedBytecode.length()), + getter_AddRefs(output)); + if (NS_FAILED(rv)) { + LOG( + ("ScriptLoadRequest (%p): Cannot open bytecode cache (rv = %X, output " + "= %p)", + aRequest, unsigned(rv), output.get())); + return; + } + MOZ_ASSERT(output); + + auto closeOutStream = mozilla::MakeScopeExit([&]() { + rv = output->CloseWithStatus(rv); + LOG(("ScriptLoadRequest (%p): Closing (rv = %X)", aRequest, unsigned(rv))); + }); + + uint32_t n; + rv = output->Write(reinterpret_cast<char*>(compressedBytecode.begin()), + compressedBytecode.length(), &n); + LOG( + ("ScriptLoadRequest (%p): Write bytecode cache (rv = %X, length = %u, " + "written = %u)", + aRequest, unsigned(rv), unsigned(compressedBytecode.length()), n)); + if (NS_FAILED(rv)) { + return; + } + + MOZ_RELEASE_ASSERT(compressedBytecode.length() == n); + + bytecodeFailed.release(); + TRACE_FOR_TEST_NONE(aRequest->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_bytecode_saved"); +} + +void ScriptLoader::GiveUpBytecodeEncoding() { + // If the document went away prematurely, we still want to set this, in order + // to avoid queuing more scripts. + mGiveUpEncoding = true; + + // Ideally we prefer to properly end the incremental encoder, such that we + // would not keep a large buffer around. If we cannot, we fallback on the + // removal of all request from the current list and these large buffers would + // be removed at the same time as the source object. + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + AutoAllowLegacyScriptExecution exemption; + Maybe<AutoEntryScript> aes; + + if (globalObject) { + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (context) { + aes.emplace(globalObject, "give-up bytecode encoding", true); + } + } + + while (!mBytecodeEncodingQueue.isEmpty()) { + RefPtr<ScriptLoadRequest> request = mBytecodeEncodingQueue.StealFirst(); + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get())); + TRACE_FOR_TEST_NONE(request->GetScriptLoadContext()->GetScriptElement(), + "scriptloader_bytecode_failed"); + MOZ_ASSERT(!IsWebExtensionRequest(request)); + + if (aes.isSome()) { + if (request->IsModuleRequest()) { + ModuleScript* moduleScript = request->AsModuleRequest()->mModuleScript; + JS::Rooted<JSObject*> module(aes->cx(), moduleScript->ModuleRecord()); + JS::AbortIncrementalEncoding(module); + } else { + JS::Rooted<JSScript*> script(aes->cx(), + request->mScriptForBytecodeEncoding); + JS::AbortIncrementalEncoding(script); + } + } + + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } +} + +bool ScriptLoader::HasPendingRequests() const { + return mParserBlockingRequest || !mXSLTRequests.isEmpty() || + !mLoadedAsyncRequests.isEmpty() || + !mNonAsyncExternalScriptInsertedRequests.isEmpty() || + !mDeferRequests.isEmpty() || HasPendingDynamicImports() || + !mPendingChildLoaders.IsEmpty(); + // mOffThreadCompilingRequests are already being processed. +} + +bool ScriptLoader::HasPendingDynamicImports() const { + if (mModuleLoader && mModuleLoader->HasPendingDynamicImports()) { + return true; + } + + for (ModuleLoader* loader : mWebExtModuleLoaders) { + if (loader->HasPendingDynamicImports()) { + return true; + } + } + + for (ModuleLoader* loader : mShadowRealmModuleLoaders) { + if (loader->HasPendingDynamicImports()) { + return true; + } + } + + return false; +} + +void ScriptLoader::ProcessPendingRequestsAsync() { + if (HasPendingRequests()) { + nsCOMPtr<nsIRunnable> task = + NewRunnableMethod("dom::ScriptLoader::ProcessPendingRequests", this, + &ScriptLoader::ProcessPendingRequests); + if (mDocument) { + mDocument->Dispatch(TaskCategory::Other, task.forget()); + } else { + NS_DispatchToCurrentThread(task.forget()); + } + } +} + +void ScriptLoader::ProcessPendingRequests() { + RefPtr<ScriptLoadRequest> request; + + if (mParserBlockingRequest && mParserBlockingRequest->IsReadyToRun() && + ReadyToExecuteParserBlockingScripts()) { + request.swap(mParserBlockingRequest); + UnblockParser(request); + ProcessRequest(request); + ContinueParserAsync(request); + } + + while (ReadyToExecuteParserBlockingScripts() && !mXSLTRequests.isEmpty() && + mXSLTRequests.getFirst()->IsReadyToRun()) { + request = mXSLTRequests.StealFirst(); + ProcessRequest(request); + } + + while (ReadyToExecuteScripts() && !mLoadedAsyncRequests.isEmpty()) { + request = mLoadedAsyncRequests.StealFirst(); + if (request->IsModuleRequest()) { + ProcessRequest(request); + } else { + CompileOffThreadOrProcessRequest(request); + } + } + + while (ReadyToExecuteScripts() && + !mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mNonAsyncExternalScriptInsertedRequests.getFirst()->IsReadyToRun()) { + // Violate the HTML5 spec and execute these in the insertion order in + // order to make LABjs and the "order" plug-in for RequireJS work with + // their Gecko-sniffed code path. See + // http://lists.w3.org/Archives/Public/public-html/2010Oct/0088.html + request = mNonAsyncExternalScriptInsertedRequests.StealFirst(); + ProcessRequest(request); + } + + if (mDeferCheckpointReached && mXSLTRequests.isEmpty()) { + while (ReadyToExecuteScripts() && !mDeferRequests.isEmpty() && + mDeferRequests.getFirst()->IsReadyToRun()) { + request = mDeferRequests.StealFirst(); + ProcessRequest(request); + } + } + + while (!mPendingChildLoaders.IsEmpty() && + ReadyToExecuteParserBlockingScripts()) { + RefPtr<ScriptLoader> child = mPendingChildLoaders[0]; + mPendingChildLoaders.RemoveElementAt(0); + child->RemoveParserBlockingScriptExecutionBlocker(); + } + + if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest && + mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mXSLTRequests.isEmpty() && mDeferRequests.isEmpty() && + MaybeRemovedDeferRequests()) { + return ProcessPendingRequests(); + } + + if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest && + mLoadingAsyncRequests.isEmpty() && mLoadedAsyncRequests.isEmpty() && + mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mXSLTRequests.isEmpty() && mDeferRequests.isEmpty()) { + // No more pending scripts; time to unblock onload. + // OK to unblock onload synchronously here, since callers must be + // prepared for the world changing anyway. + mDeferCheckpointReached = false; + mDocument->UnblockOnload(true); + } +} + +bool ScriptLoader::ReadyToExecuteParserBlockingScripts() { + // Make sure the SelfReadyToExecuteParserBlockingScripts check is first, so + // that we don't block twice on an ancestor. + if (!SelfReadyToExecuteParserBlockingScripts()) { + return false; + } + + if (mDocument && mDocument->GetWindowContext()) { + for (WindowContext* wc = + mDocument->GetWindowContext()->GetParentWindowContext(); + wc; wc = wc->GetParentWindowContext()) { + if (Document* doc = wc->GetDocument()) { + ScriptLoader* ancestor = doc->ScriptLoader(); + if (!ancestor->SelfReadyToExecuteParserBlockingScripts() && + ancestor->AddPendingChildLoader(this)) { + AddParserBlockingScriptExecutionBlocker(); + return false; + } + } + } + } + + return true; +} + +template <typename Unit> +static nsresult ConvertToUnicode(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, Unit*& aBufOut, + size_t& aLengthOut) { + if (!aLength) { + aBufOut = nullptr; + aLengthOut = 0; + return NS_OK; + } + + auto data = Span(aData, aLength); + + // The encoding info precedence is as follows from high to low: + // The BOM + // HTTP Content-Type (if name recognized) + // charset attribute (if name recognized) + // The encoding of the document + + UniquePtr<Decoder> unicodeDecoder; + + const Encoding* encoding; + std::tie(encoding, std::ignore) = Encoding::ForBOM(data); + if (encoding) { + unicodeDecoder = encoding->NewDecoderWithBOMRemoval(); + } + + if (!unicodeDecoder && aChannel) { + nsAutoCString label; + if (NS_SUCCEEDED(aChannel->GetContentCharset(label)) && + (encoding = Encoding::ForLabel(label))) { + unicodeDecoder = encoding->NewDecoderWithoutBOMHandling(); + } + } + + if (!unicodeDecoder && (encoding = Encoding::ForLabel(aHintCharset))) { + unicodeDecoder = encoding->NewDecoderWithoutBOMHandling(); + } + + if (!unicodeDecoder && aDocument) { + unicodeDecoder = + aDocument->GetDocumentCharacterSet()->NewDecoderWithoutBOMHandling(); + } + + if (!unicodeDecoder) { + // Curiously, there are various callers that don't pass aDocument. The + // fallback in the old code was ISO-8859-1, which behaved like + // windows-1252. + unicodeDecoder = WINDOWS_1252_ENCODING->NewDecoderWithoutBOMHandling(); + } + + auto signalOOM = mozilla::MakeScopeExit([&aBufOut, &aLengthOut]() { + aBufOut = nullptr; + aLengthOut = 0; + }); + + CheckedInt<size_t> bufferLength = + ScriptDecoding<Unit>::MaxBufferLength(unicodeDecoder, aLength); + if (!bufferLength.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CheckedInt<size_t> bufferByteSize = bufferLength * sizeof(Unit); + if (!bufferByteSize.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aBufOut = static_cast<Unit*>(js_malloc(bufferByteSize.value())); + if (!aBufOut) { + return NS_ERROR_OUT_OF_MEMORY; + } + + signalOOM.release(); + aLengthOut = ScriptDecoding<Unit>::DecodeInto( + unicodeDecoder, data, Span(aBufOut, bufferLength.value()), + /* aEndOfSource = */ true); + return NS_OK; +} + +/* static */ +nsresult ScriptLoader::ConvertToUTF16(nsIChannel* aChannel, + const uint8_t* aData, uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, char16_t*& aBufOut, + size_t& aLengthOut) { + return ConvertToUnicode(aChannel, aData, aLength, aHintCharset, aDocument, + aBufOut, aLengthOut); +} + +/* static */ +nsresult ScriptLoader::ConvertToUTF8(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, Utf8Unit*& aBufOut, + size_t& aLengthOut) { + return ConvertToUnicode(aChannel, aData, aLength, aHintCharset, aDocument, + aBufOut, aLengthOut); +} + +nsresult ScriptLoader::OnStreamComplete( + nsIIncrementalStreamLoader* aLoader, ScriptLoadRequest* aRequest, + nsresult aChannelStatus, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) { + NS_ASSERTION(aRequest, "null request in stream complete handler"); + NS_ENSURE_TRUE(aRequest, NS_ERROR_FAILURE); + + nsresult rv = VerifySRI(aRequest, aLoader, aSRIStatus, aSRIDataVerifier); + + if (NS_SUCCEEDED(rv)) { + // If we are loading from source, save the computed SRI hash or a dummy SRI + // hash in case we are going to save the bytecode of this script in the + // cache. + if (aRequest->IsSource()) { + uint32_t sriLength = 0; + rv = SaveSRIHash(aRequest, aSRIDataVerifier, &sriLength); + MOZ_ASSERT_IF(NS_SUCCEEDED(rv), + aRequest->mScriptBytecode.length() == sriLength); + + aRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + if (aRequest->mBytecodeOffset != sriLength) { + // We need extra padding after SRI hash. + if (!aRequest->mScriptBytecode.resize(aRequest->mBytecodeOffset)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + + if (NS_SUCCEEDED(rv)) { + rv = PrepareLoadedRequest(aRequest, aLoader, aChannelStatus); + } + + if (NS_FAILED(rv)) { + ReportErrorToConsole(aRequest, rv); + } + } + + if (NS_FAILED(rv)) { + // When loading bytecode, we verify the SRI hash. If it does not match the + // one from the document we restart the load, forcing us to load the source + // instead. If this happens do not remove the current request from script + // loader's data structures or fire any events. + if (aChannelStatus != NS_BINDING_RETARGETED) { + HandleLoadError(aRequest, rv); + } + } + + // Process our request and/or any pending ones + ProcessPendingRequests(); + + return rv; +} + +nsresult ScriptLoader::VerifySRI(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) const { + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + nsCOMPtr<nsIChannel> channel; + channel = do_QueryInterface(channelRequest); + + nsresult rv = NS_OK; + if (!aRequest->mIntegrity.IsEmpty() && NS_SUCCEEDED((rv = aSRIStatus))) { + MOZ_ASSERT(aSRIDataVerifier); + MOZ_ASSERT(mReporter); + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + rv = aSRIDataVerifier->Verify(aRequest->mIntegrity, channel, sourceUri, + mReporter); + if (channelRequest) { + mReporter->FlushReportsToConsole( + nsContentUtils::GetInnerWindowID(channelRequest)); + } else { + mReporter->FlushConsoleReports(mDocument); + } + if (NS_FAILED(rv)) { + rv = NS_ERROR_SRI_CORRUPT; + } + } + + return rv; +} + +nsresult ScriptLoader::SaveSRIHash(ScriptLoadRequest* aRequest, + SRICheckDataVerifier* aSRIDataVerifier, + uint32_t* sriLength) const { + MOZ_ASSERT(aRequest->IsSource()); + MOZ_ASSERT(aRequest->mScriptBytecode.empty()); + + uint32_t len; + + // If the integrity metadata does not correspond to a valid hash function, + // IsComplete would be false. + if (!aRequest->mIntegrity.IsEmpty() && aSRIDataVerifier->IsComplete()) { + MOZ_ASSERT(aRequest->mScriptBytecode.length() == 0); + + // Encode the SRI computed hash. + len = aSRIDataVerifier->DataSummaryLength(); + + if (!aRequest->mScriptBytecode.resize(len)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + DebugOnly<nsresult> res = aSRIDataVerifier->ExportDataSummary( + len, aRequest->mScriptBytecode.begin()); + MOZ_ASSERT(NS_SUCCEEDED(res)); + } else { + MOZ_ASSERT(aRequest->mScriptBytecode.length() == 0); + + // Encode a dummy SRI hash. + len = SRICheckDataVerifier::EmptyDataSummaryLength(); + + if (!aRequest->mScriptBytecode.resize(len)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + DebugOnly<nsresult> res = SRICheckDataVerifier::ExportEmptyDataSummary( + len, aRequest->mScriptBytecode.begin()); + MOZ_ASSERT(NS_SUCCEEDED(res)); + } + + // Verify that the exported and predicted length correspond. + DebugOnly<uint32_t> srilen{}; + MOZ_ASSERT(NS_SUCCEEDED(SRICheckDataVerifier::DataSummaryLength( + len, aRequest->mScriptBytecode.begin(), &srilen))); + MOZ_ASSERT(srilen == len); + + *sriLength = len; + + return NS_OK; +} + +void ScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const { + MOZ_ASSERT(aRequest); + + if (aRequest->GetScriptLoadContext()->IsPreload()) { + // Skip reporting errors in preload requests. If the request is actually + // used then we will report the error in ReportPreloadErrorsToConsole below. + aRequest->GetScriptLoadContext()->mUnreportedPreloadError = aResult; + return; + } + + bool isScript = !aRequest->IsModuleRequest(); + const char* message; + if (aResult == NS_ERROR_MALFORMED_URI) { + message = isScript ? "ScriptSourceMalformed" : "ModuleSourceMalformed"; + } else if (aResult == NS_ERROR_DOM_BAD_URI) { + message = isScript ? "ScriptSourceNotAllowed" : "ModuleSourceNotAllowed"; + } else if (aResult == NS_ERROR_DOM_WEBEXT_CONTENT_SCRIPT_URI) { + MOZ_ASSERT(!isScript); + message = "WebExtContentScriptModuleSourceNotAllowed"; + } else if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + aResult)) { + // Blocking classifier error codes already show their own console messages. + return; + } else { + message = isScript ? "ScriptSourceLoadFailed" : "ModuleSourceLoadFailed"; + } + + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(aRequest->mURI->GetSpecOrDefault(), *params.AppendElement()); + + nsIScriptElement* element = + aRequest->GetScriptLoadContext()->GetScriptElement(); + uint32_t lineNo = element ? element->GetScriptLineNumber() : 0; + uint32_t columnNo = element ? element->GetScriptColumnNumber() : 0; + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Script Loader"_ns, mDocument, + nsContentUtils::eDOM_PROPERTIES, message, + params, nullptr, u""_ns, lineNo, columnNo); +} + +void ScriptLoader::ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray<nsString>& aParams) const { + nsIScriptElement* element = + aRequest->GetScriptLoadContext()->GetScriptElement(); + uint32_t lineNo = element ? element->GetScriptLineNumber() : 0; + uint32_t columnNo = element ? element->GetScriptColumnNumber() : 0; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Script Loader"_ns, mDocument, + nsContentUtils::eDOM_PROPERTIES, aMessageName, + aParams, nullptr, u""_ns, lineNo, columnNo); +} + +void ScriptLoader::ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest) { + if (NS_FAILED(aRequest->GetScriptLoadContext()->mUnreportedPreloadError)) { + ReportErrorToConsole( + aRequest, aRequest->GetScriptLoadContext()->mUnreportedPreloadError); + aRequest->GetScriptLoadContext()->mUnreportedPreloadError = NS_OK; + } + + if (aRequest->IsModuleRequest()) { + for (const auto& childRequest : aRequest->AsModuleRequest()->mImports) { + ReportPreloadErrorsToConsole(childRequest); + } + } +} + +void ScriptLoader::HandleLoadError(ScriptLoadRequest* aRequest, + nsresult aResult) { + /* + * Handle script not loading error because source was an tracking URL (or + * fingerprinting, cryptomining, etc). + * We make a note of this script node by including it in a dedicated + * array of blocked tracking nodes under its parent document. + */ + if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + aResult)) { + nsCOMPtr<nsIContent> cont = + do_QueryInterface(aRequest->GetScriptLoadContext()->GetScriptElement()); + mDocument->AddBlockedNodeByClassifier(cont); + } + + if (aRequest->IsModuleRequest()) { + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mIsInline); + aRequest->AsModuleRequest()->OnFetchComplete(aResult); + } + + if (aRequest->GetScriptLoadContext()->mInDeferList) { + MOZ_ASSERT_IF(aRequest->IsModuleRequest(), + aRequest->AsModuleRequest()->IsTopLevel()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mDeferRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->GetScriptLoadContext()->mInAsyncList) { + MOZ_ASSERT_IF(aRequest->IsModuleRequest(), + aRequest->AsModuleRequest()->IsTopLevel()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mLoadingAsyncRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->GetScriptLoadContext()->mIsNonAsyncScriptInserted) { + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = + mNonAsyncExternalScriptInsertedRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->GetScriptLoadContext()->mIsXSLT) { + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mXSLTRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->GetScriptLoadContext()->IsPreload()) { + if (aRequest->IsModuleRequest()) { + aRequest->Cancel(); + } + if (aRequest->IsTopLevel()) { + // Request may already have been removed by + // CancelAndClearScriptLoadRequests. + mPreloads.RemoveElement(aRequest, PreloadRequestComparator()); + } + MOZ_ASSERT(!aRequest->isInList()); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::LoadError); + } else if (aRequest->IsModuleRequest()) { + ModuleLoadRequest* modReq = aRequest->AsModuleRequest(); + if (modReq->IsDynamicImport()) { + MOZ_ASSERT(modReq->IsTopLevel()); + if (aRequest->isInList()) { + modReq->CancelDynamicImport(aResult); + } + } else { + MOZ_ASSERT(!modReq->IsTopLevel()); + MOZ_ASSERT(!modReq->isInList()); + modReq->Cancel(); + // The error is handled for the top level module. + } + } else if (mParserBlockingRequest == aRequest) { + MOZ_ASSERT(!aRequest->isInList()); + mParserBlockingRequest = nullptr; + UnblockParser(aRequest); + + // Ensure that we treat aRequest->GetScriptLoadContext()->GetScriptElement() + // as our current parser-inserted script while firing onerror on it. + MOZ_ASSERT(aRequest->GetScriptLoadContext() + ->GetScriptElement() + ->GetParserCreated()); + nsCOMPtr<nsIScriptElement> oldParserInsertedScript = + mCurrentParserInsertedScript; + mCurrentParserInsertedScript = + aRequest->GetScriptLoadContext()->GetScriptElement(); + FireScriptAvailable(aResult, aRequest); + ContinueParserAsync(aRequest); + mCurrentParserInsertedScript = oldParserInsertedScript; + } else { + // This happens for blocking requests cancelled by ParsingComplete(). + // Ignore cancellation status for link-preload requests, as cancellation can + // be omitted for them when SRI is stronger on consumer tags. + MOZ_ASSERT(aRequest->IsCanceled() || + aRequest->GetScriptLoadContext()->IsLinkPreloadScript()); + MOZ_ASSERT(!aRequest->isInList()); + } +} + +void ScriptLoader::UnblockParser(ScriptLoadRequest* aParserBlockingRequest) { + aParserBlockingRequest->GetScriptLoadContext() + ->GetScriptElement() + ->UnblockParser(); +} + +void ScriptLoader::ContinueParserAsync( + ScriptLoadRequest* aParserBlockingRequest) { + aParserBlockingRequest->GetScriptLoadContext() + ->GetScriptElement() + ->ContinueParserAsync(); +} + +uint32_t ScriptLoader::NumberOfProcessors() { + if (mNumberOfProcessors > 0) { + return mNumberOfProcessors; + } + + int32_t numProcs = PR_GetNumberOfProcessors(); + if (numProcs > 0) { + mNumberOfProcessors = numProcs; + } + return mNumberOfProcessors; +} + +int32_t ScriptLoader::PhysicalSizeOfMemoryInGB() { + // 0 is a valid result from PR_GetPhysicalMemorySize() which + // means a failure occured. + if (mPhysicalSizeOfMemory >= 0) { + return mPhysicalSizeOfMemory; + } + + // Save the size in GB. + mPhysicalSizeOfMemory = + static_cast<int32_t>(PR_GetPhysicalMemorySize() >> 30); + return mPhysicalSizeOfMemory; +} + +static bool IsInternalURIScheme(nsIURI* uri) { + return uri->SchemeIs("moz-extension") || uri->SchemeIs("resource") || + uri->SchemeIs("chrome"); +} + +bool ScriptLoader::ShouldApplyDelazifyStrategy(ScriptLoadRequest* aRequest) { + // Full parse everything if negative. + if (StaticPrefs::dom_script_loader_delazification_max_size() < 0) { + return true; + } + + // Be conservative on machines with 2GB or less of memory. + if (PhysicalSizeOfMemoryInGB() <= + StaticPrefs::dom_script_loader_delazification_min_mem()) { + return false; + } + + uint32_t max_size = static_cast<uint32_t>( + StaticPrefs::dom_script_loader_delazification_max_size()); + uint32_t script_size = + aRequest->ScriptTextLength() > 0 + ? static_cast<uint32_t>(aRequest->ScriptTextLength()) + : 0; + + if (mTotalFullParseSize + script_size < max_size) { + return true; + } + + if (LOG_ENABLED()) { + nsCString url = aRequest->mURI->GetSpecOrDefault(); + LOG( + ("ScriptLoadRequest (%p): non-on-demand-only Parsing Disabled for (%s) " + "with size=%u because mTotalFullParseSize=%u would exceed max_size=%u", + aRequest, url.get(), script_size, mTotalFullParseSize, max_size)); + } + + return false; +} + +void ScriptLoader::ApplyDelazifyStrategy(JS::CompileOptions* aOptions) { + JS::DelazificationOption strategy = + JS::DelazificationOption::ParseEverythingEagerly; + uint32_t strategyIndex = + StaticPrefs::dom_script_loader_delazification_strategy(); + + // Assert that all enumerated values of DelazificationOption are dense between + // OnDemandOnly and ParseEverythingEagerly. +#ifdef DEBUG + uint32_t count = 0; + uint32_t mask = 0; +# define _COUNT_ENTRIES(Name) count++; +# define _MASK_ENTRIES(Name) \ + mask |= 1 << uint32_t(JS::DelazificationOption::Name); + + FOREACH_DELAZIFICATION_STRATEGY(_COUNT_ENTRIES); + MOZ_ASSERT(count == uint32_t(strategy) + 1); + FOREACH_DELAZIFICATION_STRATEGY(_MASK_ENTRIES); + MOZ_ASSERT(((mask + 1) & mask) == 0); +# undef _COUNT_ENTRIES +# undef _MASK_ENTRIES +#endif + + // Any strategy index larger than ParseEverythingEagerly would default to + // ParseEverythingEagerly. + if (strategyIndex <= uint32_t(strategy)) { + strategy = JS::DelazificationOption(uint8_t(strategyIndex)); + } + + aOptions->setEagerDelazificationStrategy(strategy); +} + +bool ScriptLoader::ShouldCompileOffThread(ScriptLoadRequest* aRequest) { + if (NumberOfProcessors() <= 1) { + return false; + } + if (aRequest == mParserBlockingRequest) { + return true; + } + if (SpeculativeOMTParsingEnabled()) { + // Processing non async inserted scripts too early can potentially delay the + // load event from firing so focus on other scripts instead. + if (aRequest->GetScriptLoadContext()->mIsNonAsyncScriptInserted && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_non_parser_inserted_enabled()) { + return false; + } + + // Async and link preload scripts do not need to be parsed right away. + if (aRequest->GetScriptLoadContext()->IsAsyncScript() && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_async_enabled()) { + return false; + } + + if (aRequest->GetScriptLoadContext()->IsLinkPreloadScript() && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_link_preload_enabled()) { + return false; + } + + return true; + } + return false; +} + +nsresult ScriptLoader::PrepareLoadedRequest(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aStatus) { + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (aRequest->IsCanceled()) { + return NS_BINDING_ABORTED; + } + MOZ_ASSERT(aRequest->IsFetching()); + CollectScriptTelemetry(aRequest); + + // If we don't have a document, then we need to abort further + // evaluation. + if (!mDocument) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If the load returned an error page, then we need to abort + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + NS_ASSERTION(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(req); + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_SUCCEEDED(rv) && !requestSucceeded) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString sourceMapURL; + if (nsContentUtils::GetSourceMapURL(httpChannel, sourceMapURL)) { + aRequest->mSourceMapURL = Some(NS_ConvertUTF8toUTF16(sourceMapURL)); + } + + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = do_QueryInterface(req); + MOZ_ASSERT(classifiedChannel); + if (classifiedChannel && + classifiedChannel->IsThirdPartyTrackingResource()) { + aRequest->GetScriptLoadContext()->SetIsTracking(); + } + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(req); + // If this load was subject to a CORS check, don't flag it with a separate + // origin principal, so that it will treat our document's principal as the + // origin principal. Module loads always use CORS. + if (!aRequest->IsModuleRequest() && aRequest->CORSMode() == CORS_NONE) { + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + channel, getter_AddRefs(aRequest->mOriginPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // This assertion could fire errorously if we ran out of memory when + // inserting the request in the array. However it's an unlikely case + // so if you see this assertion it is likely something else that is + // wrong, especially if you see it more than once. + NS_ASSERTION(mDeferRequests.Contains(aRequest) || + mLoadingAsyncRequests.Contains(aRequest) || + mNonAsyncExternalScriptInsertedRequests.Contains(aRequest) || + mXSLTRequests.Contains(aRequest) || + (aRequest->IsModuleRequest() && + (aRequest->AsModuleRequest()->IsRegisteredDynamicImport() || + !aRequest->AsModuleRequest()->IsTopLevel())) || + mPreloads.Contains(aRequest, PreloadRequestComparator()) || + mParserBlockingRequest == aRequest, + "aRequest should be pending!"); + + nsCOMPtr<nsIURI> uri; + rv = channel->GetOriginalURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + // Fixup moz-extension: and resource: URIs, because the channel URI will + // point to file:, which won't be allowed to load. + if (uri && IsInternalURIScheme(uri)) { + aRequest->mBaseURL = uri; + } else { + channel->GetURI(getter_AddRefs(aRequest->mBaseURL)); + } + + if (aRequest->IsModuleRequest()) { + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + + // When loading a module, only responses with a JavaScript MIME type are + // acceptable. + nsAutoCString mimeType; + channel->GetContentType(mimeType); + NS_ConvertUTF8toUTF16 typeString(mimeType); + if (!nsContentUtils::IsJavascriptMIMEType(typeString)) { + return NS_ERROR_FAILURE; + } + + // Attempt to compile off main thread. + bool couldCompile = false; + rv = AttemptOffThreadScriptCompile(request, &couldCompile); + NS_ENSURE_SUCCESS(rv, rv); + if (couldCompile) { + return NS_OK; + } + + // Otherwise compile it right away and start fetching descendents. + return request->OnFetchComplete(NS_OK); + } + + // The script is now loaded and ready to run. + aRequest->SetReady(); + + // If speculative parsing is enabled attempt to compile all + // external scripts off-main-thread. Otherwise, only omt compile scripts + // blocking the parser. + if (ShouldCompileOffThread(aRequest)) { + MOZ_ASSERT(!aRequest->IsModuleRequest()); + bool couldCompile = false; + nsresult rv = AttemptOffThreadScriptCompile(aRequest, &couldCompile); + NS_ENSURE_SUCCESS(rv, rv); + if (couldCompile) { + MOZ_ASSERT(aRequest->mState == ScriptLoadRequest::State::Compiling, + "Request should be off-thread compiling now."); + return NS_OK; + } + + // If off-thread compile was rejected, continue with regular processing. + } + + MaybeMoveToLoadedList(aRequest); + + return NS_OK; +} + +void ScriptLoader::DeferCheckpointReached() { + if (mDeferEnabled) { + // Have to check because we apparently get ParsingComplete + // without BeginDeferringScripts in some cases + mDeferCheckpointReached = true; + } + + mDeferEnabled = false; + ProcessPendingRequests(); +} + +void ScriptLoader::ParsingComplete(bool aTerminated) { + if (aTerminated) { + CancelAndClearScriptLoadRequests(); + + // Have to call this even if aTerminated so we'll correctly unblock onload. + DeferCheckpointReached(); + } +} + +void ScriptLoader::PreloadURI(nsIURI* aURI, const nsAString& aCharset, + const nsAString& aType, + const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, + bool aAsync, bool aDefer, bool aNoModule, + bool aLinkPreload, + const ReferrerPolicy aReferrerPolicy, + uint64_t aEarlyHintPreloaderId) { + NS_ENSURE_TRUE_VOID(mDocument); + // Check to see if scripts has been turned off. + if (!mEnabled || !mDocument->IsScriptEnabled()) { + return; + } + + ScriptKind scriptKind = ScriptKind::eClassic; + + if (mDocument->ModuleScriptsEnabled()) { + // Don't load nomodule scripts. + if (aNoModule) { + return; + } + + static const char kASCIIWhitespace[] = "\t\n\f\r "; + + nsAutoString type(aType); + type.Trim(kASCIIWhitespace); + if (type.LowerCaseEqualsASCII("module")) { + scriptKind = ScriptKind::eModule; + } + } + + if (scriptKind == ScriptKind::eClassic && !aType.IsEmpty() && + !nsContentUtils::IsJavascriptMIMEType(aType)) { + // Unknown type. Don't load it. + return; + } + + SRIMetadata sriMetadata; + GetSRIMetadata(aIntegrity, &sriMetadata); + + RefPtr<ScriptLoadRequest> request = CreateLoadRequest( + scriptKind, aURI, nullptr, mDocument->NodePrincipal(), + Element::StringToCORSMode(aCrossOrigin), sriMetadata, aReferrerPolicy); + request->GetScriptLoadContext()->mIsInline = false; + request->GetScriptLoadContext()->mScriptFromHead = aScriptFromHead; + request->GetScriptLoadContext()->SetScriptMode(aDefer, aAsync, aLinkPreload); + request->GetScriptLoadContext()->SetIsPreloadRequest(); + + if (LOG_ENABLED()) { + nsAutoCString url; + aURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Created preload request for %s", + request.get(), url.get())); + } + + nsAutoString charset(aCharset); + nsresult rv = StartLoad(request, aEarlyHintPreloaderId, Some(charset)); + if (NS_FAILED(rv)) { + return; + } + + PreloadInfo* pi = mPreloads.AppendElement(); + pi->mRequest = request; + pi->mCharset = aCharset; +} + +void ScriptLoader::AddDeferRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->GetScriptLoadContext()->IsDeferredScript()); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mInDeferList && + !aRequest->GetScriptLoadContext()->mInAsyncList); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mInCompilingList); + + aRequest->GetScriptLoadContext()->mInDeferList = true; + mDeferRequests.AppendElement(aRequest); + if (mDeferEnabled && aRequest == mDeferRequests.getFirst() && mDocument && + !mBlockingDOMContentLoaded) { + MOZ_ASSERT(mDocument->GetReadyStateEnum() == Document::READYSTATE_LOADING); + mBlockingDOMContentLoaded = true; + mDocument->BlockDOMContentLoaded(); + } +} + +void ScriptLoader::AddAsyncRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->GetScriptLoadContext()->IsAsyncScript()); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mInDeferList && + !aRequest->GetScriptLoadContext()->mInAsyncList); + MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mInCompilingList); + + aRequest->GetScriptLoadContext()->mInAsyncList = true; + if (aRequest->IsReadyToRun()) { + mLoadedAsyncRequests.AppendElement(aRequest); + } else { + mLoadingAsyncRequests.AppendElement(aRequest); + } +} + +void ScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsReadyToRun()); + MOZ_ASSERT(aRequest->IsTopLevel()); + + // If it's async, move it to the loaded list. + // aRequest->GetScriptLoadContext()->mInAsyncList really _should_ be in a + // list, but the consequences if it's not are bad enough we want to avoid + // trying to move it if it's not. + if (aRequest->GetScriptLoadContext()->mInAsyncList) { + MOZ_ASSERT(aRequest->isInList()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mLoadingAsyncRequests.Steal(aRequest); + mLoadedAsyncRequests.AppendElement(req); + } + } else if (aRequest->IsModuleRequest() && + aRequest->AsModuleRequest()->IsDynamicImport()) { + // Process dynamic imports with async scripts. + MOZ_ASSERT(!aRequest->isInList()); + mLoadedAsyncRequests.AppendElement(aRequest); + } +} + +bool ScriptLoader::MaybeRemovedDeferRequests() { + if (mDeferRequests.isEmpty() && mDocument && mBlockingDOMContentLoaded) { + mBlockingDOMContentLoaded = false; + mDocument->UnblockDOMContentLoaded(); + return true; + } + return false; +} + +DocGroup* ScriptLoader::GetDocGroup() const { return mDocument->GetDocGroup(); } + +void ScriptLoader::BeginDeferringScripts() { + mDeferEnabled = true; + if (mDeferCheckpointReached) { + // We already completed a parse and were just waiting for some async + // scripts to load (and were already blocking the load event waiting for + // that to happen), when document.open() happened and now we're doing a + // new parse. We shouldn't block the load event again, but _should_ reset + // mDeferCheckpointReached to false. It'll get set to true again when the + // DeferCheckpointReached call that corresponds to this + // BeginDeferringScripts call happens (on document.close()), since we just + // set mDeferEnabled to true. + mDeferCheckpointReached = false; + } else { + if (mDocument) { + mDocument->BlockOnload(); + } + } +} + +nsAutoScriptLoaderDisabler::nsAutoScriptLoaderDisabler(Document* aDoc) { + mLoader = aDoc->ScriptLoader(); + mWasEnabled = mLoader->GetEnabled(); + if (mWasEnabled) { + mLoader->SetEnabled(false); + } +} + +nsAutoScriptLoaderDisabler::~nsAutoScriptLoaderDisabler() { + if (mWasEnabled) { + mLoader->SetEnabled(true); + } +} + +#undef TRACE_FOR_TEST +#undef TRACE_FOR_TEST_BOOL +#undef TRACE_FOR_TEST_NONE + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/script/ScriptLoader.h b/dom/script/ScriptLoader.h new file mode 100644 index 0000000000..6ed3193a10 --- /dev/null +++ b/dom/script/ScriptLoader.h @@ -0,0 +1,802 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ScriptLoader_h +#define mozilla_dom_ScriptLoader_h + +#include "js/TypeDecls.h" +#include "js/loader/LoadedScript.h" +#include "js/loader/ScriptKind.h" +#include "js/loader/ScriptLoadRequest.h" +#include "mozilla/dom/ScriptLoadContext.h" +#include "nsCOMPtr.h" +#include "nsRefPtrHashtable.h" +#include "nsIScriptElement.h" +#include "nsCOMArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsILoadInfo.h" // nsSecurityFlags +#include "nsINode.h" +#include "nsIObserver.h" +#include "nsIScriptLoaderObserver.h" +#include "nsURIHashKey.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/JSExecutionContext.h" // JSExecutionContext +#include "ModuleLoader.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/MozPromise.h" + +class nsCycleCollectionTraversalCallback; +class nsIChannel; +class nsIConsoleReportCollector; +class nsIContent; +class nsIIncrementalStreamLoader; +class nsIPrincipal; +class nsIScriptGlobalObject; +class nsIURI; + +namespace JS { + +class CompileOptions; + +template <typename UnitT> +class SourceText; + +namespace loader { + +class LoadedScript; +class ScriptLoaderInterface; +class ModuleLoadRequest; +class ModuleScript; +class ScriptLoadRequest; +class ScriptLoadRequestList; + +} // namespace loader +} // namespace JS + +namespace mozilla { + +class LazyLogModule; +union Utf8Unit; + +namespace dom { + +class AutoJSAPI; +class DocGroup; +class Document; +class ModuleLoader; +class SRICheckDataVerifier; +class SRIMetadata; +class ScriptLoadHandler; +class ScriptLoadContext; +class ScriptLoader; +class ScriptRequestProcessor; + +enum class ReferrerPolicy : uint8_t; + +class AsyncCompileShutdownObserver final : public nsIObserver { + ~AsyncCompileShutdownObserver() { Unregister(); } + + public: + explicit AsyncCompileShutdownObserver(ScriptLoader* aLoader) + : mScriptLoader(aLoader) {} + + void OnShutdown(); + void Unregister(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + private: + // Defined during registration in ScriptLoader constructor, and + // cleared during destructor, ScriptLoader::Destroy() or Shutdown. + ScriptLoader* mScriptLoader; +}; + +////////////////////////////////////////////////////////////// +// Script loader implementation +////////////////////////////////////////////////////////////// + +class ScriptLoader final : public JS::loader::ScriptLoaderInterface { + class MOZ_STACK_CLASS AutoCurrentScriptUpdater { + public: + AutoCurrentScriptUpdater(ScriptLoader* aScriptLoader, + nsIScriptElement* aCurrentScript) + : mOldScript(aScriptLoader->mCurrentScript), + mScriptLoader(aScriptLoader) { + nsCOMPtr<nsINode> node = do_QueryInterface(aCurrentScript); + mScriptLoader->mCurrentScript = + node && !node->IsInShadowTree() ? aCurrentScript : nullptr; + } + + ~AutoCurrentScriptUpdater() { + mScriptLoader->mCurrentScript.swap(mOldScript); + } + + private: + nsCOMPtr<nsIScriptElement> mOldScript; + ScriptLoader* mScriptLoader; + }; + + friend class JS::loader::ModuleLoadRequest; + friend class ScriptRequestProcessor; + friend class ModuleLoader; + friend class ScriptLoadHandler; + friend class AutoCurrentScriptUpdater; + + public: + explicit ScriptLoader(Document* aDocument); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ScriptLoader) + + /** + * Called when the document that owns this script loader changes global. The + * argument is null when the document is detached from a window. + */ + void SetGlobalObject(nsIGlobalObject* aGlobalObject); + + /** + * The loader maintains a weak reference to the document with + * which it is initialized. This call forces the reference to + * be dropped. + */ + void DropDocumentReference() { mDocument = nullptr; } + + /** + * Add an observer for all scripts loaded through this loader. + * + * @param aObserver observer for all script processing. + */ + nsresult AddObserver(nsIScriptLoaderObserver* aObserver) { + return mObservers.AppendObject(aObserver) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + + /** + * Remove an observer. + * + * @param aObserver observer to be removed + */ + void RemoveObserver(nsIScriptLoaderObserver* aObserver) { + mObservers.RemoveObject(aObserver); + } + + /** + * Process a script element. This will include both loading the + * source of the element if it is not inline and evaluating + * the script itself. + * + * If the script is an inline script that can be executed immediately + * (i.e. there are no other scripts pending) then ScriptAvailable + * and ScriptEvaluated will be called before the function returns. + * + * If true is returned the script could not be executed immediately. + * In this case ScriptAvailable is guaranteed to be called at a later + * point (as well as possibly ScriptEvaluated). + * + * @param aElement The element representing the script to be loaded and + * evaluated. + */ + bool ProcessScriptElement(nsIScriptElement* aElement, + const nsAutoString& aTypeAttr); + + /** + * Gets the currently executing script. This is useful if you want to + * generate a unique key based on the currently executing script. + */ + nsIScriptElement* GetCurrentScript() { return mCurrentScript; } + + nsIScriptElement* GetCurrentParserInsertedScript() { + return mCurrentParserInsertedScript; + } + + /** + * Whether the loader is enabled or not. + * When disabled, processing of new script elements is disabled. + * Any call to ProcessScriptElement() will return false. Note that + * this DOES NOT disable currently loading or executing scripts. + */ + bool GetEnabled() { return mEnabled; } + + void SetEnabled(bool aEnabled) { + if (!mEnabled && aEnabled) { + ProcessPendingRequestsAsync(); + } + mEnabled = aEnabled; + } + + ModuleLoader* GetModuleLoader() { return mModuleLoader; } + + void RegisterContentScriptModuleLoader(ModuleLoader* aLoader); + void RegisterShadowRealmModuleLoader(ModuleLoader* aLoader); + + /** + * Check whether to speculatively OMT parse scripts as soon as + * they are fetched, even if not a parser blocking request. + * Controlled by + * dom.script_loader.external_scripts.speculative_omt_parse_enabled + */ + bool SpeculativeOMTParsingEnabled() const { + return mSpeculativeOMTParsingEnabled; + } + + /** + * Add/remove a blocker for parser-blocking scripts (and XSLT + * scripts). Blockers will stop such scripts from executing, but not from + * loading. + */ + void AddParserBlockingScriptExecutionBlocker() { + ++mParserBlockingBlockerCount; + } + + void RemoveParserBlockingScriptExecutionBlocker() { + if (!--mParserBlockingBlockerCount && ReadyToExecuteScripts()) { + ProcessPendingRequestsAsync(); + } + } + + /** + * Add/remove a blocker for execution of all scripts. Blockers will stop + * scripts from executing, but not from loading. + */ + void AddExecuteBlocker() { ++mBlockerCount; } + + void RemoveExecuteBlocker() { + MOZ_ASSERT(mBlockerCount); + if (!--mBlockerCount) { + ProcessPendingRequestsAsync(); + } + } + + /** + * Convert the given buffer to a UTF-16 string. If the buffer begins with a + * BOM, it is interpreted as that encoding; otherwise the first of |aChannel|, + * |aHintCharset|, or |aDocument| that provides a recognized encoding is used, + * or Windows-1252 if none of them do. + * + * Encoding errors in the buffer are converted to replacement characters, so + * allocation failure is the only way this function can fail. + * + * @param aChannel Channel corresponding to the data. May be null. + * @param aData The data to convert + * @param aLength Length of the data + * @param aHintCharset Character set hint (e.g., from a charset attribute). + * @param aDocument Document which the data is loaded for. May be null. + * @param aBufOut [out] fresh char16_t array containing data converted to + * Unicode. Caller must js_free() this data when finished + * with it. + * @param aLengthOut [out] Length of array returned in aBufOut in number + * of char16_t code units. + */ + static nsresult ConvertToUTF16(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, char16_t*& aBufOut, + size_t& aLengthOut); + + static nsresult ConvertToUTF16(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, + JS::UniqueTwoByteChars& aBufOut, + size_t& aLengthOut) { + char16_t* bufOut; + nsresult rv = ConvertToUTF16(aChannel, aData, aLength, aHintCharset, + aDocument, bufOut, aLengthOut); + if (NS_SUCCEEDED(rv)) { + aBufOut.reset(bufOut); + } + return rv; + }; + + /** + * Convert the given buffer to a UTF-8 string. If the buffer begins with a + * BOM, it is interpreted as that encoding; otherwise the first of |aChannel|, + * |aHintCharset|, or |aDocument| that provides a recognized encoding is used, + * or Windows-1252 if none of them do. + * + * Encoding errors in the buffer are converted to replacement characters, so + * allocation failure is the only way this function can fail. + * + * @param aChannel Channel corresponding to the data. May be null. + * @param aData The data to convert + * @param aLength Length of the data + * @param aHintCharset Character set hint (e.g., from a charset attribute). + * @param aDocument Document which the data is loaded for. May be null. + * @param aBufOut [out] fresh Utf8Unit array containing data converted to + * Unicode. Caller must js_free() this data when finished + * with it. + * @param aLengthOut [out] Length of array returned in aBufOut in UTF-8 code + * units (i.e. in bytes). + */ + static nsresult ConvertToUTF8(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, const nsAString& aHintCharset, + Document* aDocument, Utf8Unit*& aBufOut, + size_t& aLengthOut); + + static inline nsresult ConvertToUTF8( + nsIChannel* aChannel, const uint8_t* aData, uint32_t aLength, + const nsAString& aHintCharset, Document* aDocument, + UniquePtr<Utf8Unit[], JS::FreePolicy>& aBufOut, size_t& aLengthOut) { + Utf8Unit* bufOut; + nsresult rv = ConvertToUTF8(aChannel, aData, aLength, aHintCharset, + aDocument, bufOut, aLengthOut); + if (NS_SUCCEEDED(rv)) { + aBufOut.reset(bufOut); + } + return rv; + }; + + /** + * Handle the completion of a stream. This is called by the + * ScriptLoadHandler object which observes the IncrementalStreamLoader + * loading the script. The streamed content is expected to be stored on the + * aRequest argument. + */ + nsresult OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + ScriptLoadRequest* aRequest, + nsresult aChannelStatus, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier); + + /** + * Returns wether any request is queued, and not executed yet. + */ + bool HasPendingRequests() const; + + /** + * Returns wether there are any dynamic module import requests pending. + */ + bool HasPendingDynamicImports() const; + + /** + * Processes any pending requests that are ready for processing. + */ + void ProcessPendingRequests(); + + /** + * Starts deferring deferred scripts and puts them in the mDeferredRequests + * queue instead. + */ + void BeginDeferringScripts(); + + /** + * Notifies the script loader that parsing is done. If aTerminated is true, + * this will drop any pending scripts that haven't run yet, otherwise it will + * do nothing. + */ + void ParsingComplete(bool aTerminated); + + /** + * Notifies the script loader that the checkpoint to begin execution of defer + * scripts has been reached. This is either the end of of the document parse + * or the end of loading of parser-inserted stylesheets, whatever happens + * last. + * + * Otherwise, it will stop deferring scripts and immediately processes the + * mDeferredRequests queue. + * + * WARNING: This function will synchronously execute content scripts, so be + * prepared that the world might change around you. + */ + void DeferCheckpointReached(); + + /** + * Returns the number of pending scripts, deferred or not. + */ + uint32_t HasPendingOrCurrentScripts() { + return mCurrentScript || mParserBlockingRequest; + } + + /** + * Adds aURI to the preload list and starts loading it. + * + * @param aURI The URI of the external script. + * @param aCharset The charset parameter for the script. + * @param aType The type parameter for the script. + * @param aCrossOrigin The crossorigin attribute for the script. + * Void if not present. + * @param aIntegrity The expect hash url, if avail, of the request + * @param aScriptFromHead Whether or not the script was a child of head + */ + virtual void PreloadURI(nsIURI* aURI, const nsAString& aCharset, + const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, + bool aAsync, bool aDefer, bool aNoModule, + bool aLinkPreload, + const ReferrerPolicy aReferrerPolicy, + uint64_t aEarlyHintPreloaderId); + + /** + * Process a request that was deferred so that the script could be compiled + * off thread. + */ + nsresult ProcessOffThreadRequest(ScriptLoadRequest* aRequest); + + bool AddPendingChildLoader(ScriptLoader* aChild) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. Else, change the return type to void. + mPendingChildLoaders.AppendElement(aChild); + return true; + } + + mozilla::dom::DocGroup* GetDocGroup() const; + + /** + * Register the fact that we saw the load event, and that we need to save the + * bytecode at the next loop cycle unless new scripts are waiting in the + * pipeline. + */ + void LoadEventFired(); + + /** + * Destroy and prevent the ScriptLoader or the ScriptLoadRequests from owning + * any references to the JSScript or to the Request which might be used for + * caching the encoded bytecode. + */ + void Destroy(); + + /* + * Get the currently active script. This is used as the initiating script when + * executing timeout handler scripts. + */ + static JS::loader::LoadedScript* GetActiveScript(JSContext* aCx); + + Document* GetDocument() const { return mDocument; } + + nsIURI* GetBaseURI() const override; + + private: + ~ScriptLoader(); + + already_AddRefed<ScriptLoadRequest> CreateLoadRequest( + ScriptKind aKind, nsIURI* aURI, nsIScriptElement* aElement, + nsIPrincipal* aTriggeringPrincipal, mozilla::CORSMode aCORSMode, + const SRIMetadata& aIntegrity, ReferrerPolicy aReferrerPolicy); + + /** + * Unblocks the creator parser of the parser-blocking scripts. + */ + void UnblockParser(ScriptLoadRequest* aParserBlockingRequest); + + /** + * Asynchronously resumes the creator parser of the parser-blocking scripts. + */ + void ContinueParserAsync(ScriptLoadRequest* aParserBlockingRequest); + + bool ProcessExternalScript(nsIScriptElement* aElement, ScriptKind aScriptKind, + const nsAutoString& aTypeAttr, + nsIContent* aScriptContent); + + bool ProcessInlineScript(nsIScriptElement* aElement, ScriptKind aScriptKind); + + JS::loader::ScriptLoadRequest* LookupPreloadRequest( + nsIScriptElement* aElement, ScriptKind aScriptKind, + const SRIMetadata& aSRIMetadata); + + void GetSRIMetadata(const nsAString& aIntegrityAttr, + SRIMetadata* aMetadataOut); + + /** + * Given a script element, get the referrer policy should be applied to load + * requests. + */ + ReferrerPolicy GetReferrerPolicy(nsIScriptElement* aElement); + + /** + * Helper function to check the content policy for a given request. + */ + static nsresult CheckContentPolicy(Document* aDocument, nsISupports* aContext, + const nsAString& aType, + ScriptLoadRequest* aRequest); + + /** + * Helper function to determine whether an about: page loads a chrome: URI. + * Please note that this function only returns true if: + * * the about: page uses a ContentPrincipal with scheme about: + * * the about: page is not linkable from content + * (e.g. the function will return false for about:blank or about:srcdoc) + */ + static bool IsAboutPageLoadingChromeURI(ScriptLoadRequest* aRequest, + Document* aDocument); + + /** + * Start a load for aRequest's URI. + */ + nsresult StartLoad(ScriptLoadRequest* aRequest, + uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload); + /** + * Start a load for a classic script URI. + * Sets up the necessary security flags before calling StartLoadInternal. + */ + nsresult StartClassicLoad(ScriptLoadRequest* aRequest, + uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload); + + /** + * Start a load for a module script URI. + * + * aCharsetForPreload is only needed when this load is a preload (via + * ScriptLoader::PreloadURI), because ScriptLoadRequest doesn't + * have this information. + */ + nsresult StartLoadInternal(ScriptLoadRequest* aRequest, + nsSecurityFlags securityFlags, + uint64_t aEarlyHintPreloaderId, + const Maybe<nsAutoString>& aCharsetForPreload); + + /** + * Abort the current stream, and re-start with a new load request from scratch + * without requesting any alternate data. Returns NS_BINDING_RETARGETED on + * success, as this error code is used to abort the input stream. + */ + nsresult RestartLoad(ScriptLoadRequest* aRequest); + + void HandleLoadError(ScriptLoadRequest* aRequest, nsresult aResult); + + /** + * Process any pending requests asynchronously (i.e. off an event) if there + * are any. Note that this is a no-op if there aren't any currently pending + * requests. + * + * This function is virtual to allow cross-library calls to SetEnabled() + */ + virtual void ProcessPendingRequestsAsync(); + + /** + * If true, the loader is ready to execute parser-blocking scripts, and so are + * all its ancestors. If the loader itself is ready but some ancestor is not, + * this function will add an execute blocker and ask the ancestor to remove it + * once it becomes ready. + */ + bool ReadyToExecuteParserBlockingScripts(); + + /** + * Return whether just this loader is ready to execute parser-blocking + * scripts. + */ + bool SelfReadyToExecuteParserBlockingScripts() { + return ReadyToExecuteScripts() && !mParserBlockingBlockerCount; + } + + /** + * Return whether this loader is ready to execute scripts in general. + */ + bool ReadyToExecuteScripts() { return mEnabled && !mBlockerCount; } + + nsresult VerifySRI(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) const; + + nsresult SaveSRIHash(ScriptLoadRequest* aRequest, + SRICheckDataVerifier* aSRIDataVerifier, + uint32_t* sriLength) const; + + void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const override; + + void ReportWarningToConsole( + ScriptLoadRequest* aRequest, const char* aMessageName, + const nsTArray<nsString>& aParams = nsTArray<nsString>()) const override; + + void ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest); + + nsresult AttemptOffThreadScriptCompile(ScriptLoadRequest* aRequest, + bool* aCouldCompileOut); + + nsresult StartOffThreadCompilation(JSContext* aCx, + ScriptLoadRequest* aRequest, + JS::CompileOptions& aOptions, + Runnable* aRunnable, + JS::OffThreadToken** aTokenOut); + + static void OffThreadCompilationCompleteCallback(JS::OffThreadToken* aToken, + void* aCallbackData); + + nsresult ProcessRequest(ScriptLoadRequest* aRequest); + nsresult CompileOffThreadOrProcessRequest(ScriptLoadRequest* aRequest); + void FireScriptAvailable(nsresult aResult, ScriptLoadRequest* aRequest); + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void FireScriptEvaluated( + nsresult aResult, ScriptLoadRequest* aRequest); + + // Implements https://html.spec.whatwg.org/#execute-the-script-block + nsresult EvaluateScriptElement(ScriptLoadRequest* aRequest); + + // Handles both bytecode and text source scripts; populates exec with a + // compiled script + nsresult CompileOrDecodeClassicScript(JSContext* aCx, + JSExecutionContext& aExec, + ScriptLoadRequest* aRequest); + + static nsCString& BytecodeMimeTypeFor(ScriptLoadRequest* aRequest); + + // Decide whether to encode bytecode for given script load request, + // and store the script into the request if necessary. + // + // This method must be called before executing the script. + void MaybePrepareForBytecodeEncodingBeforeExecute( + ScriptLoadRequest* aRequest, JS::Handle<JSScript*> aScript); + + // Queue the script load request for bytecode encoding if we decided to + // encode, or cleanup the script load request fields otherwise. + // + // This method must be called after executing the script. + nsresult MaybePrepareForBytecodeEncodingAfterExecute( + ScriptLoadRequest* aRequest, nsresult aRv); + + // Returns true if MaybePrepareForBytecodeEncodingAfterExecute is called + // for given script load request. + bool IsAlreadyHandledForBytecodeEncodingPreparation( + ScriptLoadRequest* aRequest); + + void MaybePrepareModuleForBytecodeEncodingBeforeExecute( + JSContext* aCx, ModuleLoadRequest* aRequest) override; + + nsresult MaybePrepareModuleForBytecodeEncodingAfterExecute( + ModuleLoadRequest* aRequest, nsresult aRv) override; + + // Implements https://html.spec.whatwg.org/#run-a-classic-script + nsresult EvaluateScript(nsIGlobalObject* aGlobalObject, + ScriptLoadRequest* aRequest); + + /** + * Queue the current script load request to be saved, when the page + * initialization ends. The page initialization end is defined as being the + * time when the load event got received, and when no more scripts are waiting + * to be executed. + */ + void RegisterForBytecodeEncoding(ScriptLoadRequest* aRequest); + + /** + * Check if all conditions are met, i-e that the onLoad event fired and that + * no more script have to be processed. If all conditions are met, queue an + * event to encode all the bytecode and save them on the cache. + */ + void MaybeTriggerBytecodeEncoding() override; + + /** + * Iterate over all script load request and save the bytecode of executed + * functions on the cache provided by the channel. + */ + void EncodeBytecode(); + void EncodeRequestBytecode(JSContext* aCx, ScriptLoadRequest* aRequest); + + void GiveUpBytecodeEncoding(); + + already_AddRefed<nsIGlobalObject> GetGlobalForRequest( + ScriptLoadRequest* aRequest); + + already_AddRefed<nsIScriptGlobalObject> GetScriptGlobalObject(); + + // Fill in CompileOptions, as well as produce the introducer script for + // subsequent calls to UpdateDebuggerMetadata + nsresult FillCompileOptionsForRequest( + JSContext* aCx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, + JS::MutableHandle<JSScript*> aIntroductionScript) override; + + uint32_t NumberOfProcessors(); + int32_t PhysicalSizeOfMemoryInGB(); + + nsresult PrepareLoadedRequest(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aStatus); + + void AddDeferRequest(ScriptLoadRequest* aRequest); + void AddAsyncRequest(ScriptLoadRequest* aRequest); + bool MaybeRemovedDeferRequests(); + + bool ShouldApplyDelazifyStrategy(ScriptLoadRequest* aRequest); + void ApplyDelazifyStrategy(JS::CompileOptions* aOptions); + + bool ShouldCompileOffThread(ScriptLoadRequest* aRequest); + + void MaybeMoveToLoadedList(ScriptLoadRequest* aRequest); + + using MaybeSourceText = + mozilla::MaybeOneOf<JS::SourceText<char16_t>, JS::SourceText<Utf8Unit>>; + + // Returns wether we should save the bytecode of this script after the + // execution of the script. + static bool ShouldCacheBytecode(ScriptLoadRequest* aRequest); + + void RunScriptWhenSafe(ScriptLoadRequest* aRequest); + + /** + * Cancel and remove all outstanding load requests, including waiting for any + * off thread compilations to finish. + */ + void CancelAndClearScriptLoadRequests(); + + Document* mDocument; // [WEAK] + nsCOMArray<nsIScriptLoaderObserver> mObservers; + ScriptLoadRequestList mNonAsyncExternalScriptInsertedRequests; + // mLoadingAsyncRequests holds async requests while they're loading; when they + // have been loaded they are moved to mLoadedAsyncRequests. + ScriptLoadRequestList mLoadingAsyncRequests; + // mLoadedAsyncRequests holds async script requests and dynamic module import + // requests, which are processed in the same way. + ScriptLoadRequestList mLoadedAsyncRequests; + ScriptLoadRequestList mDeferRequests; + ScriptLoadRequestList mXSLTRequests; + RefPtr<ScriptLoadRequest> mParserBlockingRequest; + ScriptLoadRequestList mOffThreadCompilingRequests; + + // List of script load request that are holding a buffer which has to be saved + // on the cache. + ScriptLoadRequestList mBytecodeEncodingQueue; + + // In mRequests, the additional information here is stored by the element. + struct PreloadInfo { + RefPtr<ScriptLoadRequest> mRequest; + nsString mCharset; + }; + + friend void ImplCycleCollectionUnlink(ScriptLoader::PreloadInfo& aField); + friend void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoader::PreloadInfo& aField, const char* aName, uint32_t aFlags); + + struct PreloadRequestComparator { + bool Equals(const PreloadInfo& aPi, + ScriptLoadRequest* const& aRequest) const { + return aRequest == aPi.mRequest; + } + }; + + struct PreloadURIComparator { + bool Equals(const PreloadInfo& aPi, nsIURI* const& aURI) const; + }; + + nsTArray<PreloadInfo> mPreloads; + + nsCOMPtr<nsIScriptElement> mCurrentScript; + nsCOMPtr<nsIScriptElement> mCurrentParserInsertedScript; + nsTArray<RefPtr<ScriptLoader>> mPendingChildLoaders; + uint32_t mParserBlockingBlockerCount; + uint32_t mBlockerCount; + uint32_t mNumberOfProcessors; + uint32_t mTotalFullParseSize; + int32_t mPhysicalSizeOfMemory; + bool mEnabled; + bool mDeferEnabled; + bool mSpeculativeOMTParsingEnabled; + bool mDeferCheckpointReached; + bool mBlockingDOMContentLoaded; + bool mLoadEventFired; + bool mGiveUpEncoding; + + TimeDuration mMainThreadParseTime; + + nsCOMPtr<nsIConsoleReportCollector> mReporter; + + // ShutdownObserver for off thread compilations + RefPtr<AsyncCompileShutdownObserver> mShutdownObserver; + + RefPtr<ModuleLoader> mModuleLoader; + nsTArray<RefPtr<ModuleLoader>> mWebExtModuleLoaders; + nsTArray<RefPtr<ModuleLoader>> mShadowRealmModuleLoaders; + + // Logging + public: + static LazyLogModule gCspPRLog; + static LazyLogModule gScriptLoaderLog; +}; + +class nsAutoScriptLoaderDisabler { + public: + explicit nsAutoScriptLoaderDisabler(Document* aDoc); + + ~nsAutoScriptLoaderDisabler(); + + bool mWasEnabled; + RefPtr<ScriptLoader> mLoader; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptLoader_h diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp new file mode 100644 index 0000000000..1f2d92bcb5 --- /dev/null +++ b/dom/script/ScriptSettings.cpp @@ -0,0 +1,709 @@ +/* -*- 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/ScriptSettings.h" + +#include <utility> +#include "MainThreadUtils.h" +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" +#include "js/Conversions.h" +#include "js/ErrorReport.h" +#include "js/Exception.h" +#include "js/GCAPI.h" +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "js/Warnings.h" +#include "js/Wrapper.h" +#include "js/friend/ErrorMessages.h" +#include "js/loader/LoadedScript.h" +#include "js/loader/ScriptLoadRequest.h" +#include "jsapi.h" +#include "mozilla/Assertions.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/WorkerCommon.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsGlobalWindowInner.h" +#include "nsIGlobalObject.h" +#include "nsINode.h" +#include "nsIPrincipal.h" +#include "nsISupports.h" +#include "nsJSUtils.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" +#include "nscore.h" +#include "xpcpublic.h" + +namespace mozilla { +namespace dom { + +static MOZ_THREAD_LOCAL(ScriptSettingsStackEntry*) sScriptSettingsTLS; + +class ScriptSettingsStack { + public: + static ScriptSettingsStackEntry* Top() { return sScriptSettingsTLS.get(); } + + static void Push(ScriptSettingsStackEntry* aEntry) { + MOZ_ASSERT(!aEntry->mOlder); + // Whenever JSAPI use is disabled, the next stack entry pushed must + // not be an AutoIncumbentScript. + MOZ_ASSERT_IF(!Top() || Top()->NoJSAPI(), !aEntry->IsIncumbentScript()); + // Whenever the top entry is not an incumbent canidate, the next stack entry + // pushed must not be an AutoIncumbentScript. + MOZ_ASSERT_IF(Top() && !Top()->IsIncumbentCandidate(), + !aEntry->IsIncumbentScript()); + + aEntry->mOlder = Top(); + sScriptSettingsTLS.set(aEntry); + } + + static void Pop(ScriptSettingsStackEntry* aEntry) { + MOZ_ASSERT(aEntry == Top()); + sScriptSettingsTLS.set(aEntry->mOlder); + } + + static nsIGlobalObject* IncumbentGlobal() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (entry->IsIncumbentCandidate()) { + return entry->mGlobalObject; + } + entry = entry->mOlder; + } + return nullptr; + } + + static ScriptSettingsStackEntry* EntryPoint() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (entry->IsEntryCandidate()) { + return entry; + } + entry = entry->mOlder; + } + return nullptr; + } + + static nsIGlobalObject* EntryGlobal() { + ScriptSettingsStackEntry* entry = EntryPoint(); + if (!entry) { + return nullptr; + } + return entry->mGlobalObject; + } + +#ifdef DEBUG + static ScriptSettingsStackEntry* TopNonIncumbentScript() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (!entry->IsIncumbentScript()) { + return entry; + } + entry = entry->mOlder; + } + return nullptr; + } +#endif // DEBUG +}; + +void InitScriptSettings() { + bool success = sScriptSettingsTLS.init(); + if (!success) { + MOZ_CRASH(); + } + + sScriptSettingsTLS.set(nullptr); +} + +void DestroyScriptSettings() { + MOZ_ASSERT(sScriptSettingsTLS.get() == nullptr); +} + +ScriptSettingsStackEntry::ScriptSettingsStackEntry(nsIGlobalObject* aGlobal, + Type aType) + : mGlobalObject(aGlobal), mType(aType), mOlder(nullptr) { + MOZ_ASSERT_IF(IsIncumbentCandidate() && !NoJSAPI(), mGlobalObject); + MOZ_ASSERT(!mGlobalObject || mGlobalObject->HasJSGlobal(), + "Must have an actual JS global for the duration on the stack"); + MOZ_ASSERT( + !mGlobalObject || + JS_IsGlobalObject(mGlobalObject->GetGlobalJSObjectPreserveColor()), + "No outer windows allowed"); +} + +ScriptSettingsStackEntry::~ScriptSettingsStackEntry() { + // We must have an actual JS global for the entire time this is on the stack. + MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal()); +} + +// If the entry or incumbent global ends up being something that the subject +// principal doesn't subsume, we don't want to use it. This never happens on +// the web, but can happen with asymmetric privilege relationships (i.e. +// ExpandedPrincipal and System Principal). +// +// The most correct thing to use instead would be the topmost global on the +// callstack whose principal is subsumed by the subject principal. But that's +// hard to compute, so we just substitute the global of the current +// compartment. In practice, this is fine. +// +// Note that in particular things like: +// +// |SpecialPowers.wrap(crossOriginWindow).eval(open())| +// +// trigger this case. Although both the entry global and the current global +// have normal principals, the use of Gecko-specific System-Principaled JS +// puts the code from two different origins on the callstack at once, which +// doesn't happen normally on the web. +static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) { + if (!aGlobalOrNull || !NS_IsMainThread()) { + return aGlobalOrNull; + } + + nsIPrincipal* globalPrin = aGlobalOrNull->PrincipalOrNull(); + NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal()); + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller() + ->SubsumesConsideringDomain(globalPrin)) { + return GetCurrentGlobal(); + } + + return aGlobalOrNull; +} + +nsIGlobalObject* GetEntryGlobal() { + return ClampToSubject(ScriptSettingsStack::EntryGlobal()); +} + +Document* GetEntryDocument() { + nsIGlobalObject* global = GetEntryGlobal(); + nsCOMPtr<nsPIDOMWindowInner> entryWin = do_QueryInterface(global); + + return entryWin ? entryWin->GetExtantDoc() : nullptr; +} + +nsIGlobalObject* GetIncumbentGlobal() { + // We need the current JSContext in order to check the JS for + // scripted frames that may have appeared since anyone last + // manipulated the stack. If it's null, that means that there + // must be no entry global on the stack, and therefore no incumbent + // global either. + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + MOZ_ASSERT(ScriptSettingsStack::EntryGlobal() == nullptr); + return nullptr; + } + + // See what the JS engine has to say. If we've got a scripted caller + // override in place, the JS engine will lie to us and pretend that + // there's nothing on the JS stack, which will cause us to check the + // incumbent script stack below. + if (JSObject* global = JS::GetScriptedCallerGlobal(cx)) { + return ClampToSubject(xpc::NativeGlobal(global)); + } + + // Ok, nothing from the JS engine. Let's use whatever's on the + // explicit stack. + return ClampToSubject(ScriptSettingsStack::IncumbentGlobal()); +} + +nsIGlobalObject* GetCurrentGlobal() { + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + return nullptr; + } + + JSObject* global = JS::CurrentGlobalOrNull(cx); + if (!global) { + return nullptr; + } + + return xpc::NativeGlobal(global); +} + +nsIPrincipal* GetWebIDLCallerPrincipal() { + MOZ_ASSERT(NS_IsMainThread()); + ScriptSettingsStackEntry* entry = ScriptSettingsStack::EntryPoint(); + + // If we have an entry point that is not NoJSAPI, we know it must be an + // AutoEntryScript. + if (!entry || entry->NoJSAPI()) { + return nullptr; + } + AutoEntryScript* aes = static_cast<AutoEntryScript*>(entry); + + return aes->mWebIDLCallerPrincipal; +} + +bool IsJSAPIActive() { + ScriptSettingsStackEntry* topEntry = ScriptSettingsStack::Top(); + return topEntry && !topEntry->NoJSAPI(); +} + +namespace danger { +JSContext* GetJSContext() { return CycleCollectedJSContext::Get()->Context(); } +} // namespace danger + +JS::RootingContext* RootingCx() { + return CycleCollectedJSContext::Get()->RootingCx(); +} + +AutoJSAPI::AutoJSAPI() + : ScriptSettingsStackEntry(nullptr, eJSAPI), + mCx(nullptr), + mIsMainThread(false) // For lack of anything better +{} + +AutoJSAPI::~AutoJSAPI() { + if (!mCx) { + // No need to do anything here: we never managed to Init, so can't have an + // exception on our (nonexistent) JSContext. We also don't need to restore + // any state on it. Finally, we never made it to pushing ourselves onto the + // ScriptSettingsStack, so shouldn't pop. + MOZ_ASSERT(ScriptSettingsStack::Top() != this); + return; + } + + ReportException(); + + if (mOldWarningReporter.isSome()) { + JS::SetWarningReporter(cx(), mOldWarningReporter.value()); + } + + ScriptSettingsStack::Pop(this); +} + +void WarningOnlyErrorReporter(JSContext* aCx, JSErrorReport* aRep); + +void AutoJSAPI::InitInternal(nsIGlobalObject* aGlobalObject, JSObject* aGlobal, + JSContext* aCx, bool aIsMainThread) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aCx == danger::GetJSContext()); + MOZ_ASSERT(aIsMainThread == NS_IsMainThread()); + MOZ_ASSERT(bool(aGlobalObject) == bool(aGlobal)); + MOZ_ASSERT_IF(aGlobalObject, + aGlobalObject->GetGlobalJSObjectPreserveColor() == aGlobal); +#ifdef DEBUG + bool haveException = JS_IsExceptionPending(aCx); +#endif // DEBUG + + mCx = aCx; + mIsMainThread = aIsMainThread; + if (aGlobal) { + JS::AssertObjectIsNotGray(aGlobal); + } + mAutoNullableRealm.emplace(mCx, aGlobal); + mGlobalObject = aGlobalObject; + + ScriptSettingsStack::Push(this); + + mOldWarningReporter.emplace(JS::GetWarningReporter(aCx)); + + JS::SetWarningReporter(aCx, WarningOnlyErrorReporter); + +#ifdef DEBUG + if (haveException) { + JS::Rooted<JS::Value> exn(aCx); + JS_GetPendingException(aCx, &exn); + + JS_ClearPendingException(aCx); + if (exn.isObject()) { + JS::Rooted<JSObject*> exnObj(aCx, &exn.toObject()); + + // Make sure we can actually read things from it. This UncheckedUwrap is + // safe because we're only getting data for a debug printf. In + // particular, we do not expose this data to anyone, which is very + // important; otherwise it could be a cross-origin information leak. + exnObj = js::UncheckedUnwrap(exnObj); + JSAutoRealm ar(aCx, exnObj); + + nsAutoJSString stack, filename, name, message; + int32_t line; + + JS::Rooted<JS::Value> tmp(aCx); + if (!JS_GetProperty(aCx, exnObj, "filename", &tmp)) { + JS_ClearPendingException(aCx); + } + if (tmp.isUndefined()) { + if (!JS_GetProperty(aCx, exnObj, "fileName", &tmp)) { + JS_ClearPendingException(aCx); + } + } + + if (!filename.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "stack", &tmp) || + !stack.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "name", &tmp) || !name.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "message", &tmp) || + !message.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "lineNumber", &tmp) || + !JS::ToInt32(aCx, tmp, &line)) { + JS_ClearPendingException(aCx); + line = 0; + } + + printf_stderr("PREEXISTING EXCEPTION OBJECT: '%s: %s'\n%s:%d\n%s\n", + NS_ConvertUTF16toUTF8(name).get(), + NS_ConvertUTF16toUTF8(message).get(), + NS_ConvertUTF16toUTF8(filename).get(), line, + NS_ConvertUTF16toUTF8(stack).get()); + } else { + // It's a primitive... not much we can do other than stringify it. + nsAutoJSString exnStr; + if (!exnStr.init(aCx, exn)) { + JS_ClearPendingException(aCx); + } + + printf_stderr("PREEXISTING EXCEPTION PRIMITIVE: %s\n", + NS_ConvertUTF16toUTF8(exnStr).get()); + } + MOZ_ASSERT(false, "We had an exception; we should not have"); + } +#endif // DEBUG +} + +AutoJSAPI::AutoJSAPI(nsIGlobalObject* aGlobalObject, bool aIsMainThread, + Type aType) + : ScriptSettingsStackEntry(aGlobalObject, aType), + mIsMainThread(aIsMainThread) { + MOZ_ASSERT(aGlobalObject); + MOZ_ASSERT(aGlobalObject->HasJSGlobal(), "Must have a JS global"); + MOZ_ASSERT(aIsMainThread == NS_IsMainThread()); + + InitInternal(aGlobalObject, aGlobalObject->GetGlobalJSObject(), + danger::GetJSContext(), aIsMainThread); +} + +void AutoJSAPI::Init() { + MOZ_ASSERT(!mCx, "An AutoJSAPI should only be initialised once"); + + InitInternal(/* aGlobalObject */ nullptr, /* aGlobal */ nullptr, + danger::GetJSContext(), NS_IsMainThread()); +} + +bool AutoJSAPI::Init(nsIGlobalObject* aGlobalObject, JSContext* aCx) { + MOZ_ASSERT(!mCx, "An AutoJSAPI should only be initialised once"); + MOZ_ASSERT(aCx); + + if (NS_WARN_IF(!aGlobalObject)) { + return false; + } + + JSObject* global = aGlobalObject->GetGlobalJSObject(); + if (NS_WARN_IF(!global)) { + return false; + } + + InitInternal(aGlobalObject, global, aCx, NS_IsMainThread()); + return true; +} + +bool AutoJSAPI::Init(nsIGlobalObject* aGlobalObject) { + return Init(aGlobalObject, danger::GetJSContext()); +} + +bool AutoJSAPI::Init(JSObject* aObject) { + MOZ_ASSERT(!js::IsCrossCompartmentWrapper(aObject)); + return Init(xpc::NativeGlobal(aObject)); +} + +bool AutoJSAPI::Init(nsPIDOMWindowInner* aWindow, JSContext* aCx) { + return Init(nsGlobalWindowInner::Cast(aWindow), aCx); +} + +bool AutoJSAPI::Init(nsPIDOMWindowInner* aWindow) { + return Init(nsGlobalWindowInner::Cast(aWindow)); +} + +bool AutoJSAPI::Init(nsGlobalWindowInner* aWindow, JSContext* aCx) { + return Init(static_cast<nsIGlobalObject*>(aWindow), aCx); +} + +bool AutoJSAPI::Init(nsGlobalWindowInner* aWindow) { + return Init(static_cast<nsIGlobalObject*>(aWindow)); +} + +// Even with autoJSAPIOwnsErrorReporting, the JS engine still sends warning +// reports to the JSErrorReporter as soon as they are generated. These go +// directly to the console, so we can handle them easily here. +// +// Eventually, SpiderMonkey will have a special-purpose callback for warnings +// only. +void WarningOnlyErrorReporter(JSContext* aCx, JSErrorReport* aRep) { + MOZ_ASSERT(aRep->isWarning()); + if (!NS_IsMainThread()) { + // Reporting a warning on workers is a bit complicated because we have to + // climb our parent chain until we get to the main thread. So go ahead and + // just go through the worker or worklet ReportError codepath here. + // + // That said, it feels like we should be able to short-circuit things a bit + // here by posting an appropriate runnable to the main thread directly... + // Worth looking into sometime. + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(aCx); + MOZ_ASSERT(ccjscx); + + ccjscx->ReportError(aRep, JS::ConstUTF8CharsZ()); + return; + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + nsGlobalWindowInner* win = xpc::CurrentWindowOrNull(aCx); + xpcReport->Init(aRep, nullptr, nsContentUtils::IsSystemCaller(aCx), + win ? win->WindowID() : 0); + xpcReport->LogToConsole(); +} + +void AutoJSAPI::ReportException() { + if (!HasException()) { + return; + } + + // AutoJSAPI uses a JSAutoNullableRealm, and may be in a null realm + // when the destructor is called. However, the JS engine requires us + // to be in a realm when we fetch the pending exception. In this case, + // we enter the privileged junk scope and don't dispatch any error events. + JS::Rooted<JSObject*> errorGlobal(cx(), JS::CurrentGlobalOrNull(cx())); + if (!errorGlobal) { + if (mIsMainThread) { + errorGlobal = xpc::PrivilegedJunkScope(); + } else { + errorGlobal = GetCurrentThreadWorkerGlobal(); + if (!errorGlobal) { + // We might be reporting an error in debugger code that ran before the + // worker's global was created. Use the debugger global instead. + errorGlobal = GetCurrentThreadWorkerDebuggerGlobal(); + if (NS_WARN_IF(!errorGlobal)) { + // An exception may have been thrown on attempt to create a global + // and now there is no realm from which to fetch the exception. + // Give up. + ClearException(); + return; + } + } + } + } + MOZ_ASSERT(JS_IsGlobalObject(errorGlobal)); + JSAutoRealm ar(cx(), errorGlobal); + JS::ExceptionStack exnStack(cx()); + JS::ErrorReportBuilder jsReport(cx()); + if (StealExceptionAndStack(&exnStack) && + jsReport.init(cx(), exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + if (mIsMainThread) { + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + + RefPtr<nsGlobalWindowInner> inner = xpc::WindowOrNull(errorGlobal); + + // For WebExtension content script, `WindowOrNull` method will return + // null, whereas we would still like to flag the exception with the + // related WindowGlobal the content script executed against. So we only + // update the `innerWindowID` and not `inner` as we don't want to dispatch + // exceptions caused by the content script to the webpage. + uint64_t innerWindowID = 0; + if (inner) { + innerWindowID = inner->WindowID(); + } else if (nsGlobalWindowInner* win = xpc::SandboxWindowOrNull( + JS::GetNonCCWObjectGlobal(errorGlobal), cx())) { + innerWindowID = win->WindowID(); + } + + bool isChrome = + nsContentUtils::ObjectPrincipal(errorGlobal)->IsSystemPrincipal(); + xpcReport->Init(jsReport.report(), jsReport.toStringResult().c_str(), + isChrome, innerWindowID); + if (inner && jsReport.report()->errorNumber != JSMSG_OUT_OF_MEMORY) { + JS::RootingContext* rcx = JS::RootingContext::get(cx()); + DispatchScriptErrorEvent(inner, rcx, xpcReport, exnStack.exception(), + exnStack.stack()); + } else { + JS::Rooted<JSObject*> stack(cx()); + JS::Rooted<JSObject*> stackGlobal(cx()); + xpc::FindExceptionStackForConsoleReport(inner, exnStack.exception(), + exnStack.stack(), &stack, + &stackGlobal); + // This error is not associated with a specific window, + // so omit the exception value to mitigate potential leaks. + xpcReport->LogToConsoleWithStack(inner, JS::NothingHandleValue, stack, + stackGlobal); + } + } else { + // On a worker or worklet, we just use the error reporting mechanism and + // don't bother with xpc::ErrorReport. This will ensure that all the + // right worker events (which are a lot more complicated than in the + // window case) get fired. + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(cx()); + MOZ_ASSERT(ccjscx); + // Before invoking ReportError, put the exception back on the context, + // because it may want to put it in its error events and has no other way + // to get hold of it. After we invoke ReportError, clear the exception on + // cx(), just in case ReportError didn't. + JS::SetPendingExceptionStack(cx(), exnStack); + ccjscx->ReportError(jsReport.report(), jsReport.toStringResult()); + ClearException(); + } + } else { + NS_WARNING("OOMed while acquiring uncaught exception from JSAPI"); + ClearException(); + } +} + +bool AutoJSAPI::PeekException(JS::MutableHandle<JS::Value> aVal) { + MOZ_ASSERT_IF(mIsMainThread, IsStackTop()); + MOZ_ASSERT(HasException()); + MOZ_ASSERT(js::GetContextRealm(cx())); + return JS_GetPendingException(cx(), aVal); +} + +bool AutoJSAPI::StealException(JS::MutableHandle<JS::Value> aVal) { + JS::ExceptionStack exnStack(cx()); + if (!StealExceptionAndStack(&exnStack)) { + return false; + } + aVal.set(exnStack.exception()); + return true; +} + +bool AutoJSAPI::StealExceptionAndStack(JS::ExceptionStack* aExnStack) { + MOZ_ASSERT_IF(mIsMainThread, IsStackTop()); + MOZ_ASSERT(HasException()); + MOZ_ASSERT(js::GetContextRealm(cx())); + + return JS::StealPendingExceptionStack(cx(), aExnStack); +} + +#ifdef DEBUG +bool AutoJSAPI::IsStackTop() const { + return ScriptSettingsStack::TopNonIncumbentScript() == this; +} +#endif // DEBUG + +AutoIncumbentScript::AutoIncumbentScript(nsIGlobalObject* aGlobalObject) + : ScriptSettingsStackEntry(aGlobalObject, eIncumbentScript), + mCallerOverride(nsContentUtils::GetCurrentJSContext()) { + ScriptSettingsStack::Push(this); +} + +AutoIncumbentScript::~AutoIncumbentScript() { ScriptSettingsStack::Pop(this); } + +AutoNoJSAPI::AutoNoJSAPI(JSContext* aCx) + : ScriptSettingsStackEntry(nullptr, eNoJSAPI), + JSAutoNullableRealm(aCx, nullptr), + mCx(aCx) { + // Make sure we don't seem to have an incumbent global due to + // whatever script is running right now. + JS::HideScriptedCaller(aCx); + + // Make sure the fallback GetIncumbentGlobal() behavior and + // GetEntryGlobal() both return null. + ScriptSettingsStack::Push(this); +} + +AutoNoJSAPI::~AutoNoJSAPI() { + ScriptSettingsStack::Pop(this); + JS::UnhideScriptedCaller(mCx); +} + +} // namespace dom + +AutoJSContext::AutoJSContext() : mCx(nullptr) { + JS::AutoSuppressGCAnalysis nogc; + MOZ_ASSERT(!mCx, "mCx should not be initialized!"); + MOZ_ASSERT(NS_IsMainThread()); + + if (dom::IsJSAPIActive()) { + mCx = dom::danger::GetJSContext(); + } else { + mJSAPI.Init(); + mCx = mJSAPI.cx(); + } +} + +AutoJSContext::operator JSContext*() const { return mCx; } + +AutoSafeJSContext::AutoSafeJSContext() : AutoJSAPI() { + MOZ_ASSERT(NS_IsMainThread()); + + DebugOnly<bool> ok = Init(xpc::UnprivilegedJunkScope()); + MOZ_ASSERT(ok, + "This is quite odd. We should have crashed in the " + "xpc::NativeGlobal() call if xpc::UnprivilegedJunkScope() " + "returned null, and inited correctly otherwise!"); +} + +AutoSlowOperation::AutoSlowOperation() : mIsMainThread(NS_IsMainThread()) { + if (mIsMainThread) { + mScriptActivity.emplace(true); + } +} + +void AutoSlowOperation::CheckForInterrupt() { + // For now we support only main thread! + if (mIsMainThread) { + // JS_CheckForInterrupt expects us to be in a realm, so we use a junk scope. + // In principle, it doesn't matter which one we use, since we aren't really + // running scripts here, and none of our interrupt callbacks can stop + // scripts in a junk scope anyway. In practice, though, the privileged junk + // scope is the same as the JSM global, and therefore always exists, while + // the unprivileged junk scope is created lazily, and may not exist until we + // try to use it. So we use the former for the sake of efficiency. + dom::AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JS_CheckForInterrupt(jsapi.cx()); + } +} + +AutoAllowLegacyScriptExecution::AutoAllowLegacyScriptExecution() { +#ifdef DEBUG + // no need to do that dance if we are off the main thread, + // because we only assert if we are on the main thread! + if (!NS_IsMainThread()) { + return; + } + sAutoAllowLegacyScriptExecution++; +#endif +} + +AutoAllowLegacyScriptExecution::~AutoAllowLegacyScriptExecution() { +#ifdef DEBUG + // no need to do that dance if we are off the main thread, + // because we only assert if we are on the main thread! + if (!NS_IsMainThread()) { + return; + } + sAutoAllowLegacyScriptExecution--; + MOZ_ASSERT(sAutoAllowLegacyScriptExecution >= 0, + "how can the stack guard produce a value less than 0?"); +#endif +} + +int AutoAllowLegacyScriptExecution::sAutoAllowLegacyScriptExecution = 0; + +/*static*/ +bool AutoAllowLegacyScriptExecution::IsAllowed() { + return sAutoAllowLegacyScriptExecution > 0; +} + +} // namespace mozilla diff --git a/dom/script/ScriptSettings.h b/dom/script/ScriptSettings.h new file mode 100644 index 0000000000..423da6c92a --- /dev/null +++ b/dom/script/ScriptSettings.h @@ -0,0 +1,429 @@ +/* -*- 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/. */ + +/* Utilities for managing the script settings object stack defined in webapps */ + +#ifndef mozilla_dom_ScriptSettings_h +#define mozilla_dom_ScriptSettings_h + +#include "xpcpublic.h" + +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/Maybe.h" + +#include "jsapi.h" +#include "js/Exception.h" +#include "js/Warnings.h" // JS::WarningReporter + +class JSObject; +class nsIGlobalObject; +class nsIPrincipal; +class nsPIDOMWindowInner; +class nsGlobalWindowInner; +class nsIScriptContext; +struct JSContext; + +namespace JS { +class ExceptionStack; +class Value; +} // namespace JS + +namespace mozilla { +namespace dom { + +class Document; + +/* + * Per thread setup/teardown routines. Init and Destroy should be invoked + * once each, at startup and shutdown of the script runtime (respectively). + */ +void InitScriptSettings(); +void DestroyScriptSettings(); + +// To implement a web-compatible browser, it is often necessary to obtain the +// global object that is "associated" with the currently-running code. This +// process is made more complicated by the fact that, historically, different +// algorithms have operated with different definitions of the "associated" +// global. +// +// HTML5 formalizes this into two concepts: the "incumbent global" and the +// "entry global". The incumbent global corresponds to the global of the +// current script being executed, whereas the entry global corresponds to the +// global of the script where the current JS execution began. +// +// There is also a potentially-distinct third global that is determined by the +// current compartment. This roughly corresponds with the notion of Realms in +// ECMAScript. +// +// Suppose some event triggers an event listener in window |A|, which invokes a +// scripted function in window |B|, which invokes the |window.location.href| +// setter in window |C|. The entry global would be |A|, the incumbent global +// would be |B|, and the current compartment would be that of |C|. +// +// In general, it's best to use to use the most-closely-associated global +// unless the spec says to do otherwise. In 95% of the cases, the global of +// the current compartment (GetCurrentGlobal()) is the right thing. For +// example, WebIDL constructors (new C.XMLHttpRequest()) are initialized with +// the global of the current compartment (i.e. |C|). +// +// The incumbent global is very similar, but differs in a few edge cases. For +// example, if window |B| does |C.location.href = "..."|, the incumbent global +// used for the navigation algorithm is B, because no script from |C| was ever +// run. +// +// The entry global is used for various things like computing base URIs, mostly +// for historical reasons. +// +// Note that all of these functions return bonafide global objects. This means +// that, for Windows, they always return the inner. + +// Returns the global associated with the top-most Candidate Entry Point on +// the Script Settings Stack. See the HTML spec. This may be null. +nsIGlobalObject* GetEntryGlobal(); + +// If the entry global is a window, returns its extant document. Otherwise, +// returns null. +Document* GetEntryDocument(); + +// Returns the global associated with the top-most entry of the the Script +// Settings Stack. See the HTML spec. This may be null. +nsIGlobalObject* GetIncumbentGlobal(); + +// Returns the global associated with the current compartment. This may be null. +nsIGlobalObject* GetCurrentGlobal(); + +// JS-implemented WebIDL presents an interesting situation with respect to the +// subject principal. A regular C++-implemented API can simply examine the +// compartment of the most-recently-executed script, and use that to infer the +// responsible party. However, JS-implemented APIs are run with system +// principal, and thus clobber the subject principal of the script that +// invoked the API. So we have to do some extra work to keep track of this +// information. +// +// We therefore implement the following behavior: +// * Each Script Settings Object has an optional WebIDL Caller Principal field. +// This defaults to null. +// * When we push an Entry Point in preparation to run a JS-implemented WebIDL +// callback, we grab the subject principal at the time of invocation, and +// store that as the WebIDL Caller Principal. +// * When non-null, callers can query this principal from script via an API on +// Components.utils. +nsIPrincipal* GetWebIDLCallerPrincipal(); + +// Returns whether JSAPI is active right now. If it is not, working with a +// JSContext you grab from somewhere random is not OK and you should be doing +// AutoJSAPI or AutoEntryScript to get yourself a properly set up JSContext. +bool IsJSAPIActive(); + +namespace danger { + +// Get the JSContext for this thread. This is in the "danger" namespace because +// we generally want people using AutoJSAPI instead, unless they really know +// what they're doing. +JSContext* GetJSContext(); + +} // namespace danger + +JS::RootingContext* RootingCx(); + +class ScriptSettingsStack; +class ScriptSettingsStackEntry { + friend class ScriptSettingsStack; + + public: + ~ScriptSettingsStackEntry(); + + bool NoJSAPI() const { return mType == eNoJSAPI; } + bool IsEntryCandidate() const { + return mType == eEntryScript || mType == eNoJSAPI; + } + bool IsIncumbentCandidate() { return mType != eJSAPI; } + bool IsIncumbentScript() { return mType == eIncumbentScript; } + + protected: + enum Type { eEntryScript, eIncumbentScript, eJSAPI, eNoJSAPI }; + + ScriptSettingsStackEntry(nsIGlobalObject* aGlobal, Type aEntryType); + + nsCOMPtr<nsIGlobalObject> mGlobalObject; + Type mType; + + private: + ScriptSettingsStackEntry* mOlder; +}; + +/* + * For any interaction with JSAPI, an AutoJSAPI (or one of its subclasses) + * must be on the stack. + * + * This base class should be instantiated as-is when the caller wants to use + * JSAPI but doesn't expect to run script. The caller must then call one of its + * Init functions before being able to access the JSContext through cx(). + * Its current duties are as-follows (see individual Init comments for details): + * + * * Grabbing an appropriate JSContext, and, on the main thread, pushing it onto + * the JSContext stack. + * * Entering an initial (possibly null) compartment, to ensure that the + * previously entered compartment for that JSContext is not used by mistake. + * * Reporting any exceptions left on the JSRuntime, unless the caller steals + * or silences them. + * + * Additionally, the following duties are planned, but not yet implemented: + * + * * De-poisoning the JSRuntime to allow manipulation of JSAPI. This requires + * implementing the poisoning first. For now, this de-poisoning + * effectively corresponds to having a non-null cx on the stack. + * + * In situations where the consumer expects to run script, AutoEntryScript + * should be used, which does additional manipulation of the script settings + * stack. In bug 991758, we'll add hard invariants to SpiderMonkey, such that + * any attempt to run script without an AutoEntryScript on the stack will + * fail. This prevents system code from accidentally triggering script + * execution at inopportune moments via surreptitious getters and proxies. + */ +class MOZ_STACK_CLASS AutoJSAPI : protected ScriptSettingsStackEntry { + public: + // Trivial constructor. One of the Init functions must be called before + // accessing the JSContext through cx(). + AutoJSAPI(); + + ~AutoJSAPI(); + + // This uses the SafeJSContext (or worker equivalent), and enters a null + // compartment, so that the consumer is forced to select a compartment to + // enter before manipulating objects. + // + // This variant will ensure that any errors reported by this AutoJSAPI as it + // comes off the stack will not fire error events or be associated with any + // particular web-visible global. + void Init(); + + // This uses the SafeJSContext (or worker equivalent), and enters the + // compartment of aGlobalObject. + // If aGlobalObject or its associated JS global are null then it returns + // false and use of cx() will cause an assertion. + // + // If aGlobalObject represents a web-visible global, errors reported by this + // AutoJSAPI as it comes off the stack will fire the relevant error events and + // show up in the corresponding web console. + // + // Successfully initializing the AutoJSAPI will ensure that it enters the + // Realm of aGlobalObject's JSObject and exposes that JSObject to active JS. + [[nodiscard]] bool Init(nsIGlobalObject* aGlobalObject); + + // This is a helper that grabs the native global associated with aObject and + // invokes the above Init() with that. aObject must not be a cross-compartment + // wrapper: CCWs are not associated with a single global. + [[nodiscard]] bool Init(JSObject* aObject); + + // Unsurprisingly, this uses aCx and enters the compartment of aGlobalObject. + // If aGlobalObject or its associated JS global are null then it returns + // false and use of cx() will cause an assertion. + // If aCx is null it will cause an assertion. + // + // If aGlobalObject represents a web-visible global, errors reported by this + // AutoJSAPI as it comes off the stack will fire the relevant error events and + // show up in the corresponding web console. + [[nodiscard]] bool Init(nsIGlobalObject* aGlobalObject, JSContext* aCx); + + // Convenience functions to take an nsPIDOMWindowInner or nsGlobalWindowInner, + // when it is more easily available than an nsIGlobalObject. + [[nodiscard]] bool Init(nsPIDOMWindowInner* aWindow); + [[nodiscard]] bool Init(nsPIDOMWindowInner* aWindow, JSContext* aCx); + + [[nodiscard]] bool Init(nsGlobalWindowInner* aWindow); + [[nodiscard]] bool Init(nsGlobalWindowInner* aWindow, JSContext* aCx); + + JSContext* cx() const { + MOZ_ASSERT(mCx, "Must call Init before using an AutoJSAPI"); + MOZ_ASSERT(IsStackTop()); + return mCx; + } + +#ifdef DEBUG + bool IsStackTop() const; +#endif + + // If HasException, report it. Otherwise, a no-op. + void ReportException(); + + bool HasException() const { + MOZ_ASSERT(IsStackTop()); + return JS_IsExceptionPending(cx()); + }; + + // Transfers ownership of the current exception from the JS engine to the + // caller. Callers must ensure that HasException() is true, and that cx() + // is in a non-null compartment. + // + // Note that this fails if and only if we OOM while wrapping the exception + // into the current compartment. + [[nodiscard]] bool StealException(JS::MutableHandle<JS::Value> aVal); + + // As for StealException(), but uses the JS::ExceptionStack class to also + // include the exception's stack, represented by SavedFrames. + [[nodiscard]] bool StealExceptionAndStack(JS::ExceptionStack* aExnStack); + + // Peek the current exception from the JS engine, without stealing it. + // Callers must ensure that HasException() is true, and that cx() is in a + // non-null compartment. + // + // Note that this fails if and only if we OOM while wrapping the exception + // into the current compartment. + [[nodiscard]] bool PeekException(JS::MutableHandle<JS::Value> aVal); + + void ClearException() { + MOZ_ASSERT(IsStackTop()); + JS_ClearPendingException(cx()); + } + + protected: + // Protected constructor for subclasses. This constructor initialises the + // AutoJSAPI, so Init must NOT be called on subclasses that use this. + AutoJSAPI(nsIGlobalObject* aGlobalObject, bool aIsMainThread, Type aType); + + mozilla::Maybe<JSAutoNullableRealm> mAutoNullableRealm; + JSContext* mCx; + + // Whether we're mainthread or not; set when we're initialized. + bool mIsMainThread; + Maybe<JS::WarningReporter> mOldWarningReporter; + + private: + void InitInternal(nsIGlobalObject* aGlobalObject, JSObject* aGlobal, + JSContext* aCx, bool aIsMainThread); + + AutoJSAPI(const AutoJSAPI&) = delete; + AutoJSAPI& operator=(const AutoJSAPI&) = delete; +}; + +/* + * A class that can be used to force a particular incumbent script on the stack. + */ +class AutoIncumbentScript : protected ScriptSettingsStackEntry { + public: + explicit AutoIncumbentScript(nsIGlobalObject* aGlobalObject); + ~AutoIncumbentScript(); + + private: + JS::AutoHideScriptedCaller mCallerOverride; +}; + +/* + * A class to put the JS engine in an unusable state. The subject principal + * will become System, the information on the script settings stack is + * rendered inaccessible, and JSAPI may not be manipulated until the class is + * either popped or an AutoJSAPI instance is subsequently pushed. + * + * This class may not be instantiated if an exception is pending. + */ +class AutoNoJSAPI : protected ScriptSettingsStackEntry, + protected JSAutoNullableRealm { + public: + AutoNoJSAPI() : AutoNoJSAPI(danger::GetJSContext()) {} + ~AutoNoJSAPI(); + + private: + // Helper constructor to avoid doing GetJSContext() multiple times + // during construction. + explicit AutoNoJSAPI(JSContext* aCx); + + // Stashed JSContext* so we don't need to GetJSContext in our destructor. + // It's probably safe to hold on to this, in the sense that the world should + // not get torn down while we're on the stack, and if it's not, we'd need to + // fix JSAutoNullableRealm to not hold on to a JSContext either, or + // something. + JSContext* mCx; + + AutoYieldJSThreadExecution mExecutionYield; +}; + +} // namespace dom + +/** + * Use AutoJSContext when you need a JS context on the stack but don't have one + * passed as a parameter. AutoJSContext will take care of finding the most + * appropriate JS context and release it when leaving the stack. + */ +class MOZ_RAII AutoJSContext { + public: + explicit AutoJSContext(); + operator JSContext*() const; + + protected: + JSContext* mCx; + dom::AutoJSAPI mJSAPI; +}; + +/** + * AutoSafeJSContext is similar to AutoJSContext but will only return the safe + * JS context. That means it will never call + * nsContentUtils::GetCurrentJSContext(). + * + * Note - This is deprecated. Please use AutoJSAPI instead. + */ +class MOZ_RAII AutoSafeJSContext : public dom::AutoJSAPI { + public: + explicit AutoSafeJSContext(); + operator JSContext*() const { return cx(); } + + private: +}; + +/** + * Use AutoSlowOperation when native side calls many JS callbacks in a row + * and slow script dialog should be activated if too much time is spent going + * through those callbacks. + * AutoSlowOperation puts an AutoScriptActivity on the stack so that we don't + * continue to reset the watchdog. CheckForInterrupt can then be used to check + * whether JS execution should be interrupted. + * This class (including CheckForInterrupt) is a no-op when used off the main + * thread. + */ +class MOZ_RAII AutoSlowOperation { + public: + explicit AutoSlowOperation(); + void CheckForInterrupt(); + + private: + bool mIsMainThread; + Maybe<xpc::AutoScriptActivity> mScriptActivity; +}; + +/** + * A class to disable interrupt callback temporary. + */ +class MOZ_RAII AutoDisableJSInterruptCallback { + public: + explicit AutoDisableJSInterruptCallback(JSContext* aCx) + : mCx(aCx), mOld(JS_DisableInterruptCallback(aCx)) {} + + ~AutoDisableJSInterruptCallback() { JS_ResetInterruptCallback(mCx, mOld); } + + private: + JSContext* mCx; + bool mOld; +}; + +/** + * A helper class which allows to allow-list legacy callers executing script + * in the AutoEntryScript constructor. The goal is to remove these exceptions + * one by one. Do not add a new one without review from a DOM peer. + */ +class MOZ_RAII AutoAllowLegacyScriptExecution { + public: + AutoAllowLegacyScriptExecution(); + ~AutoAllowLegacyScriptExecution(); + + static bool IsAllowed(); + + private: + static int sAutoAllowLegacyScriptExecution; +}; + +} // namespace mozilla + +#endif // mozilla_dom_ScriptSettings_h diff --git a/dom/script/ScriptTrace.h b/dom/script/ScriptTrace.h new file mode 100644 index 0000000000..29562d4f18 --- /dev/null +++ b/dom/script/ScriptTrace.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ScriptTrace_h +#define mozilla_dom_ScriptTrace_h + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/StaticPrefs_dom.h" + +// This macro is used to wrap a tracing mechanism which is scheduling events +// which are then used by the JavaScript code of test cases to track the code +// path to verify the optimizations are working as expected. +#define TRACE_FOR_TEST(elem, str) \ + PR_BEGIN_MACRO \ + nsresult rv = NS_OK; \ + rv = mozilla::dom::script::TestingDispatchEvent( \ + elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + NS_ENSURE_SUCCESS(rv, rv); \ + PR_END_MACRO + +#define TRACE_FOR_TEST_BOOL(elem, str) \ + PR_BEGIN_MACRO \ + nsresult rv = NS_OK; \ + rv = mozilla::dom::script::TestingDispatchEvent( \ + elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + NS_ENSURE_SUCCESS(rv, false); \ + PR_END_MACRO + +#define TRACE_FOR_TEST_NONE(elem, str) \ + PR_BEGIN_MACRO \ + mozilla::dom::script::TestingDispatchEvent( \ + elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + PR_END_MACRO + +namespace mozilla::dom::script { + +static nsresult TestingDispatchEvent(nsIScriptElement* aScriptElement, + const nsAString& aEventType) { + if (!StaticPrefs::dom_expose_test_interfaces()) { + return NS_OK; + } + + nsCOMPtr<nsINode> target(do_QueryInterface(aScriptElement)); + if (!target) { + return NS_OK; + } + + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + target, aEventType, CanBubble::eYes, ChromeOnlyDispatch::eNo); + return dispatcher->PostDOMEvent(); +} +} // namespace mozilla::dom::script + +#endif // mozilla_dom_ScriptTrace_h diff --git a/dom/script/ShadowRealmGlobalScope.cpp b/dom/script/ShadowRealmGlobalScope.cpp new file mode 100644 index 0000000000..992263aaa5 --- /dev/null +++ b/dom/script/ShadowRealmGlobalScope.cpp @@ -0,0 +1,126 @@ +/* -*- 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 "nsGlobalWindowInner.h" +#include "nsIGlobalObject.h" +#include "xpcpublic.h" +#include "js/TypeDecls.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ModuleLoader.h" +#include "mozilla/dom/ShadowRealmGlobalScope.h" +#include "mozilla/dom/ShadowRealmGlobalScopeBinding.h" + +#include "js/loader/ModuleLoaderBase.h" + +using namespace JS::loader; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ShadowRealmGlobalScope, mModuleLoader, + mCreatingGlobal) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ShadowRealmGlobalScope) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ShadowRealmGlobalScope) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ShadowRealmGlobalScope) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIGlobalObject) + NS_INTERFACE_MAP_ENTRY(ShadowRealmGlobalScope) +NS_INTERFACE_MAP_END + +JSObject* NewShadowRealmGlobal(JSContext* aCx, JS::RealmOptions& aOptions, + JSPrincipals* aPrincipals, + JS::Handle<JSObject*> aGlobalObj) { + JS::Rooted<JSObject*> reflector(aCx); + { + RefPtr<ShadowRealmGlobalScope> scope; + GlobalObject global(aCx, aGlobalObj); + + nsCOMPtr<nsIGlobalObject> nsGlobal = + do_QueryInterface(global.GetAsSupports()); + + MOZ_ASSERT(nsGlobal); + + scope = new ShadowRealmGlobalScope(nsGlobal); + ShadowRealmGlobalScope_Binding::Wrap(aCx, scope, scope, aOptions, + aPrincipals, true, &reflector); + } + + return reflector; +} + +static nsIGlobalObject* FindEnclosingNonShadowRealmGlobal( + ShadowRealmGlobalScope* scope) { + nsCOMPtr<nsIGlobalObject> global = scope->GetCreatingGlobal(); + + do { + nsCOMPtr<ShadowRealmGlobalScope> shadowRealmGlobalScope = + do_QueryInterface(global); + if (!shadowRealmGlobalScope) { + break; + } + + // Our global was a ShadowRealmGlobal; that's a problem, as we can't find a + // window or worker global associated with a ShadowRealmGlobal... so we + // continue following the chain. + // + // This will happen if you have nested ShadowRealms. + global = shadowRealmGlobalScope->GetCreatingGlobal(); + } while (true); + + return global; +} + +ModuleLoaderBase* ShadowRealmGlobalScope::GetModuleLoader(JSContext* aCx) { + if (mModuleLoader) { + return mModuleLoader; + } + + // Note: if this fails, we don't need to report an exception, as one will be + // reported by ModuleLoaderBase::GetCurrentModuleLoader. + + // Don't bother asking the ShadowRealmGlobal itself for a host object to get a + // module loader from, instead, delegate to the enclosing global of the shadow + // realm + nsCOMPtr<nsIGlobalObject> global = FindEnclosingNonShadowRealmGlobal(this); + MOZ_RELEASE_ASSERT(global); + + JSObject* object = global->GetGlobalJSObject(); + MOZ_ASSERT(object); + + // Currently Workers will never get here, because dynamic import is disabled + // in Worker context, and so importValue will throw before we get here. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1247687 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=1772162 + nsGlobalWindowInner* window = xpc::WindowGlobalOrNull(object); + if (!window) { + return nullptr; + } + + RefPtr<Document> doc = window->GetExtantDoc(); + if (!doc) { + return nullptr; + } + + ScriptLoader* scriptLoader = doc->ScriptLoader(); + + mModuleLoader = new ModuleLoader(scriptLoader, this, ModuleLoader::Normal); + + // Register the shadow realm module loader for tracing and ownership. + scriptLoader->RegisterShadowRealmModuleLoader( + static_cast<ModuleLoader*>(mModuleLoader.get())); + + return mModuleLoader; +} + +bool IsShadowRealmGlobal(JSObject* aObject) { + return IS_INSTANCE_OF(ShadowRealmGlobalScope, aObject); +} + +} // namespace mozilla::dom diff --git a/dom/script/ShadowRealmGlobalScope.h b/dom/script/ShadowRealmGlobalScope.h new file mode 100644 index 0000000000..eb6ffd2267 --- /dev/null +++ b/dom/script/ShadowRealmGlobalScope.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ShadowRealmGlobalScope_h +#define mozilla_dom_ShadowRealmGlobalScope_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/OriginTrials.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsContentUtils.h" +#include "nsIGlobalObject.h" +#include "nsWrapperCache.h" + +#include "js/loader/ModuleLoaderBase.h" + +namespace mozilla::dom { + +#define SHADOWREALMGLOBALSCOPE_IID \ + { /* 1b0a59dd-c1cb-429a-bb90-cea17994dba2 */ \ + 0x1b0a59dd, 0xc1cb, 0x429a, { \ + 0xbb, 0x90, 0xce, 0xa1, 0x79, 0x94, 0xdb, 0xa2 \ + } \ + } + +// Required for providing the wrapper, as this is the global used inside a Gecko +// backed ShadowRealm, but also required to power module resolution. +class ShadowRealmGlobalScope : public nsIGlobalObject, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ShadowRealmGlobalScope) + + NS_DECLARE_STATIC_IID_ACCESSOR(SHADOWREALMGLOBALSCOPE_IID) + + explicit ShadowRealmGlobalScope(nsIGlobalObject* aCreatingGlobal) + : mCreatingGlobal(aCreatingGlobal){}; + + nsIGlobalObject* GetCreatingGlobal() const { return mCreatingGlobal; } + OriginTrials Trials() const override { return {}; } + + JSObject* GetGlobalJSObject() override { return GetWrapper(); } + JSObject* GetGlobalJSObjectPreserveColor() const override { + return GetWrapperPreserveColor(); + } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + MOZ_CRASH("Shouldn't be here"); + return nullptr; + } + + JS::loader::ModuleLoaderBase* GetModuleLoader(JSContext* aCx) override; + + bool ShouldResistFingerprinting( + RFPTarget aTarget = RFPTarget::Unknown) const override { + return nsContentUtils::ShouldResistFingerprinting( + "Presently we don't have enough context to make an informed decision" + "on JS Sandboxes. See 1782853", + aTarget); + } + + private: + virtual ~ShadowRealmGlobalScope() = default; + + RefPtr<JS::loader::ModuleLoaderBase> mModuleLoader; + + // The global which created this ShadowRealm + nsCOMPtr<nsIGlobalObject> mCreatingGlobal; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ShadowRealmGlobalScope, + SHADOWREALMGLOBALSCOPE_IID) + +JSObject* NewShadowRealmGlobal(JSContext* aCx, JS::RealmOptions& aOptions, + JSPrincipals* aPrincipals, + JS::Handle<JSObject*> aGlobalObj); + +bool IsShadowRealmGlobal(JSObject* aObject); + +} // namespace mozilla::dom + +#endif diff --git a/dom/script/moz.build b/dom/script/moz.build new file mode 100644 index 0000000000..17fa695342 --- /dev/null +++ b/dom/script/moz.build @@ -0,0 +1,54 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIScriptLoaderObserver.idl", +] + +XPIDL_MODULE = "dom" + +EXPORTS += [ + "nsIScriptElement.h", +] + +EXPORTS.mozilla.dom += [ + "AutoEntryScript.h", + "ModuleLoader.h", + "ScriptCompression.h", + "ScriptDecoding.h", + "ScriptElement.h", + "ScriptLoadContext.h", + "ScriptLoader.h", + "ScriptLoadHandler.h", + "ScriptSettings.h", + "ScriptTrace.h", + "ShadowRealmGlobalScope.h", +] + +UNIFIED_SOURCES += [ + "AutoEntryScript.cpp", + "ModuleLoader.cpp", + "nsIScriptElement.cpp", + "ScriptCompression.cpp", + "ScriptElement.cpp", + "ScriptLoadContext.cpp", + "ScriptLoader.cpp", + "ScriptLoadHandler.cpp", + "ScriptSettings.cpp", + "ShadowRealmGlobalScope.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/js/loader", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/script/nsIScriptElement.cpp b/dom/script/nsIScriptElement.cpp new file mode 100644 index 0000000000..d0b6282874 --- /dev/null +++ b/dom/script/nsIScriptElement.cpp @@ -0,0 +1,53 @@ + +/* -*- 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 "nsIScriptElement.h" + +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "nsIParser.h" +#include "nsIWeakReference.h" + +void nsIScriptElement::SetCreatorParser(nsIParser* aParser) { + mCreatorParser = do_GetWeakReference(aParser); +} + +void nsIScriptElement::UnblockParser() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->UnblockParser(); + } +} + +void nsIScriptElement::ContinueParserAsync() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->ContinueInterruptedParsingAsync(); + } +} + +void nsIScriptElement::BeginEvaluating() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->IncrementScriptNestingLevel(); + } +} + +void nsIScriptElement::EndEvaluating() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->DecrementScriptNestingLevel(); + } +} + +already_AddRefed<nsIParser> nsIScriptElement::GetCreatorParser() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + return parser.forget(); +} + +mozilla::dom::ReferrerPolicy nsIScriptElement::GetReferrerPolicy() { + return mozilla::dom::ReferrerPolicy::_empty; +} diff --git a/dom/script/nsIScriptElement.h b/dom/script/nsIScriptElement.h new file mode 100644 index 0000000000..31c4cf948f --- /dev/null +++ b/dom/script/nsIScriptElement.h @@ -0,0 +1,359 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIScriptElement_h___ +#define nsIScriptElement_h___ + +#include "js/loader/ScriptKind.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/FromParser.h" +#include "nsCOMPtr.h" +#include "nsID.h" +#include "nsIScriptLoaderObserver.h" +#include "nsIWeakReferenceUtils.h" +#include "nsStringFwd.h" +#include "nscore.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsIPrincipal.h" + +class nsIContent; +class nsIParser; +class nsIPrincipal; +class nsIURI; + +namespace mozilla::dom { +class Document; +enum class ReferrerPolicy : uint8_t; +} // namespace mozilla::dom + +// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs +#define NS_ISCRIPTELEMENT_IID \ + { \ + 0xe60fca9b, 0x1b96, 0x4e4e, { \ + 0xa9, 0xb4, 0xdc, 0x98, 0x4f, 0x88, 0x3f, 0x9c \ + } \ + } + +/** + * Internal interface implemented by script elements + */ +class nsIScriptElement : public nsIScriptLoaderObserver { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISCRIPTELEMENT_IID) + + explicit nsIScriptElement(mozilla::dom::FromParser aFromParser) + : mLineNumber(1), + mColumnNumber(1), + mAlreadyStarted(false), + mMalformed(false), + mDoneAddingChildren(aFromParser == mozilla::dom::NOT_FROM_PARSER || + aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT), + mForceAsync(aFromParser == mozilla::dom::NOT_FROM_PARSER || + aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT), + mFrozen(false), + mDefer(false), + mAsync(false), + mExternal(false), + mKind(JS::loader::ScriptKind::eClassic), + mParserCreated(aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT + ? mozilla::dom::NOT_FROM_PARSER + : aFromParser), + // Fragment parser-created scripts (if executable) + // behave like script-created scripts. + mCreatorParser(nullptr) {} + + /** + * Content type identifying the scripting language. Can be empty, in + * which case javascript will be assumed. + * Return false if type attribute is not found. + */ + virtual bool GetScriptType(nsAString& type) = 0; + + /** + * Location of script source text. Can return null, in which case + * this is assumed to be an inline script element. + */ + nsIURI* GetScriptURI() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mUri; + } + + nsIPrincipal* GetScriptURITriggeringPrincipal() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mSrcTriggeringPrincipal; + } + + /** + * Script source text for inline script elements. + */ + virtual void GetScriptText(nsAString& text) const = 0; + + virtual void GetScriptCharset(nsAString& charset) = 0; + + /** + * Freezes the return values of the following methods so that subsequent + * modifications to the attributes don't change execution behavior: + * - GetScriptIsModule() + * - GetScriptIsImportMap() + * - GetScriptDeferred() + * - GetScriptAsync() + * - GetScriptURI() + * - GetScriptExternal() + */ + virtual void FreezeExecutionAttrs(mozilla::dom::Document*) = 0; + + /** + * Is the script a module script. Currently only supported by HTML scripts. + */ + bool GetScriptIsModule() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mKind == JS::loader::ScriptKind::eModule; + } + + /** + * Is the script an import map. Currently only supported by HTML scripts. + */ + bool GetScriptIsImportMap() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mKind == JS::loader::ScriptKind::eImportMap; + } + + /** + * Is the script deferred. Currently only supported by HTML scripts. + */ + bool GetScriptDeferred() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mDefer; + } + + /** + * Is the script async. Currently only supported by HTML scripts. + */ + bool GetScriptAsync() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mAsync; + } + + /** + * Is the script an external script? + */ + bool GetScriptExternal() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mExternal; + } + + /** + * Returns how the element was created. + */ + mozilla::dom::FromParser GetParserCreated() { return mParserCreated; } + + void SetScriptLineNumber(uint32_t aLineNumber) { mLineNumber = aLineNumber; } + + uint32_t GetScriptLineNumber() { return mLineNumber; } + + void SetScriptColumnNumber(uint32_t aColumnNumber) { + mColumnNumber = aColumnNumber; + } + + uint32_t GetScriptColumnNumber() { return mColumnNumber; } + + void SetIsMalformed() { mMalformed = true; } + + bool IsMalformed() { return mMalformed; } + + void PreventExecution() { mAlreadyStarted = true; } + + void LoseParserInsertedness() { + mUri = nullptr; + mCreatorParser = nullptr; + mParserCreated = mozilla::dom::NOT_FROM_PARSER; + mForceAsync = !GetAsyncState(); + + // Reset state set by FreezeExecutionAttrs(). + mFrozen = false; + mExternal = false; + mAsync = false; + mDefer = false; + mKind = JS::loader::ScriptKind::eClassic; + } + + void SetCreatorParser(nsIParser* aParser); + + /** + * Unblocks the creator parser + */ + void UnblockParser(); + + /** + * Attempts to resume parsing asynchronously + */ + void ContinueParserAsync(); + + /** + * Informs the creator parser that the evaluation of this script is starting + */ + void BeginEvaluating(); + + /** + * Informs the creator parser that the evaluation of this script is ending + */ + void EndEvaluating(); + + /** + * Retrieves a pointer to the creator parser if this has one or null if not + */ + already_AddRefed<nsIParser> GetCreatorParser(); + + /** + * This method is called when the parser finishes creating the script + * element's children, if any are present. + * + * @return whether the parser will be blocked while this script is being + * loaded + */ + bool AttemptToExecute() { + mDoneAddingChildren = true; + bool block = MaybeProcessScript(); + if (!mAlreadyStarted) { + // Need to lose parser-insertedness here to allow another script to cause + // execution later. + LoseParserInsertedness(); + } + return block; + } + + /** + * Get the CORS mode of the script element + */ + virtual mozilla::CORSMode GetCORSMode() const { + /* Default to no CORS */ + return mozilla::CORS_NONE; + } + + /** + * Get referrer policy of the script element + */ + virtual mozilla::dom::ReferrerPolicy GetReferrerPolicy(); + + /** + * Fire an error event + */ + virtual nsresult FireErrorEvent() = 0; + + protected: + /** + * Processes the script if it's in the document-tree and links to or + * contains a script. Once it has been evaluated there is no way to make it + * reevaluate the script, you'll have to create a new element. This also means + * that when adding a src attribute to an element that already contains an + * inline script, the script referenced by the src attribute will not be + * loaded. + * + * In order to be able to use multiple childNodes, or to use the + * fallback mechanism of using both inline script and linked script you have + * to add all attributes and childNodes before adding the element to the + * document-tree. + * + * @return whether the parser will be blocked while this script is being + * loaded + */ + virtual bool MaybeProcessScript() = 0; + + /** + * Since we've removed the XPCOM interface to HTML elements, we need a way to + * retreive async state from script elements without bringing the type in. + */ + virtual bool GetAsyncState() = 0; + + /** + * Allow implementing elements to avoid unnecessary QueryReferences. + */ + virtual nsIContent* GetAsContent() = 0; + + /** + * The start line number of the script. + */ + uint32_t mLineNumber; + + /** + * The start column number of the script. + */ + uint32_t mColumnNumber; + + /** + * The "already started" flag per HTML5. + */ + bool mAlreadyStarted; + + /** + * The script didn't have an end tag. + */ + bool mMalformed; + + /** + * False if parser-inserted but the parser hasn't triggered running yet. + */ + bool mDoneAddingChildren; + + /** + * If true, the .async property returns true instead of reflecting the + * content attribute. + */ + bool mForceAsync; + + /** + * Whether src, defer and async are frozen. + */ + bool mFrozen; + + /** + * The effective deferredness. + */ + bool mDefer; + + /** + * The effective asyncness. + */ + bool mAsync; + + /** + * The effective externalness. A script can be external with mUri being null + * if the src attribute contained an invalid URL string. + */ + bool mExternal; + + /** + * The effective script kind. + */ + JS::loader::ScriptKind mKind; + + /** + * Whether this element was parser-created. + */ + mozilla::dom::FromParser mParserCreated; + + /** + * The effective src (or null if no src). + */ + nsCOMPtr<nsIURI> mUri; + + /** + * The triggering principal for the src URL. + */ + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + + /** + * The creator parser of a non-defer, non-async parser-inserted script. + */ + nsWeakPtr mCreatorParser; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIScriptElement, NS_ISCRIPTELEMENT_IID) + +#endif // nsIScriptElement_h___ diff --git a/dom/script/nsIScriptLoaderObserver.idl b/dom/script/nsIScriptLoaderObserver.idl new file mode 100644 index 0000000000..ceeb79cb39 --- /dev/null +++ b/dom/script/nsIScriptLoaderObserver.idl @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIScriptElement; +interface nsIURI; + +[scriptable, uuid(7b787204-76fb-4764-96f1-fb7a666db4f4)] +interface nsIScriptLoaderObserver : nsISupports { + + /** + * The script is available for evaluation. For inline scripts, this + * method will be called synchronously. For externally loaded scripts, + * this method will be called when the load completes. + * + * @param aResult A result code representing the result of loading + * a script. If this is a failure code, script evaluation + * will not occur. + * @param aElement The element being processed. + * @param aIsInline Is this an inline classic script (as opposed to an + * externally loaded classic script or module script)? + * @param aURI What is the URI of the script (the document URI if + * it is inline). + * @param aLineNo At what line does the script appear (generally 1 + * if it is a loaded script). + */ + void scriptAvailable(in nsresult aResult, + in nsIScriptElement aElement, + in boolean aIsInlineClassicScript, + in nsIURI aURI, + in uint32_t aLineNo); + + /** + * The script has been evaluated. + * + * @param aResult A result code representing the success or failure of + * the script evaluation. + * @param aElement The element being processed. + * @param aIsInline Is this an inline script or externally loaded? + */ + [can_run_script] + void scriptEvaluated(in nsresult aResult, + in nsIScriptElement aElement, + in boolean aIsInline); +}; |