diff options
Diffstat (limited to 'dom/script/ModuleLoader.cpp')
-rw-r--r-- | dom/script/ModuleLoader.cpp | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/dom/script/ModuleLoader.cpp b/dom/script/ModuleLoader.cpp new file mode 100644 index 0000000000..5d6b90cbf0 --- /dev/null +++ b/dom/script/ModuleLoader.cpp @@ -0,0 +1,371 @@ +/* -*- 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/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 "mozilla/dom/RequestBinding.h" +#include "mozilla/Assertions.h" +#include "nsError.h" +#include "xpcpublic.h" +#include "GeckoProfiler.h" +#include "nsContentSecurityManager.h" +#include "nsIContent.h" +#include "nsJSUtils.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsIPrincipal.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Maybe.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, 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::AsyncExecuteInlineModule(ModuleLoadRequest* aRequest) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread( + mozilla::NewRunnableMethod<RefPtr<ModuleLoadRequest>>( + "ModuleLoader::ExecuteInlineModule", this, + &ModuleLoader::ExecuteInlineModule, aRequest))); +} + +void ModuleLoader::ExecuteInlineModule(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsFinished()); + MOZ_ASSERT(aRequest->IsTopLevel()); + MOZ_ASSERT(aRequest->GetScriptLoadContext()->mIsInline); + + if (aRequest->GetScriptLoadContext()->GetParserCreated() == NOT_FROM_PARSER) { + GetScriptLoader()->RunScriptWhenSafe(aRequest); + } else { + GetScriptLoader()->MaybeMoveToLoadedList(aRequest); + GetScriptLoader()->ProcessPendingRequests(); + } + + aRequest->GetScriptLoadContext()->MaybeUnblockOnload(); +} + +void ModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsFinished()); + + if (aRequest->IsTopLevel()) { + if (aRequest->GetScriptLoadContext()->mIsInline && + aRequest->GetScriptLoadContext()->GetParserCreated() == + NOT_FROM_PARSER) { + if (aRequest->mImports.Length() == 0) { + GetScriptLoader()->RunScriptWhenSafe(aRequest); + } else { + AsyncExecuteInlineModule(aRequest); + return; + } + } else if (aRequest->GetScriptLoadContext()->mIsInline && + aRequest->GetScriptLoadContext()->GetParserCreated() != + NOT_FROM_PARSER && + !nsContentUtils::IsSafeToRunScript()) { + // Avoid giving inline async module scripts that don't have + // external dependencies a guaranteed execution time relative + // to the HTML parse. That is, deliberately avoid guaranteeing + // that the script would always observe a DOM shape where the + // parser has not added further elements to the DOM. + // (If `nsContentUtils::IsSafeToRunScript()` returns `true`, + // we come here synchronously from the parser. If it returns + // `false` we come here from an external dependency completing + // its fetch, in which case we already are at an unspecific + // point relative to the parse.) + AsyncExecuteInlineModule(aRequest); + return; + } else { + GetScriptLoader()->MaybeMoveToLoadedList(aRequest); + GetScriptLoader()->ProcessPendingRequestsAsync(); + } + } + + 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::InstantiationStorage storage; + RefPtr<JS::Stencil> stencil = + aRequest->GetScriptLoadContext()->StealOffThreadResult(aCx, &storage); + if (!stencil) { + return NS_ERROR_FAILURE; + } + + JS::InstantiateOptions instantiateOptions(aOptions); + aModuleOut.set(JS::InstantiateModuleStencil(aCx, instantiateOptions, + stencil, &storage)); + 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, + aRequest->mLoadContext.get()); + 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; + + JS::TranscodeRange range = aRequest->Bytecode(); + 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, ReferrerPolicy aReferrerPolicy, + ScriptFetchOptions* aFetchOptions, const SRIMetadata& aIntegrity, + nsIURI* aReferrer, ScriptLoader* aLoader, ScriptLoadContext* aContext) { + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, aReferrerPolicy, aFetchOptions, aIntegrity, aReferrer, aContext, + true, + /* is top level */ false, /* is dynamic import */ + aLoader->GetModuleLoader(), + ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr); + + request->NoCacheEntryFound(); + 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->ReferrerPolicy(), aParent->mFetchOptions, SRIMetadata(), + aParent->mURI, newContext, false, /* is top level */ + false, /* is dynamic import */ + aParent->mLoader, aParent->mVisitedSet, aParent->GetRootModule()); + + request->NoCacheEntryFound(); + return request.forget(); +} + +already_AddRefed<ModuleLoadRequest> ModuleLoader::CreateDynamicImport( + JSContext* aCx, nsIURI* aURI, LoadedScript* aMaybeActiveScript, + 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(); + ReferrerPolicy referrerPolicy; + + if (aMaybeActiveScript) { + // https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule + // Step 6.3. Set fetchOptions to the new descendant script fetch options for + // referencingScript's fetch options. + options = aMaybeActiveScript->GetFetchOptions(); + referrerPolicy = aMaybeActiveScript->ReferrerPolicy(); + 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()); + + // https://html.spec.whatwg.org/multipage/webappapis.html#hostloadimportedmodule + // Step 4. Let fetchOptions be the default classic script fetch options. + // + // https://html.spec.whatwg.org/multipage/webappapis.html#default-classic-script-fetch-options + // The default classic script fetch options are a script fetch options whose + // cryptographic nonce is the empty string, integrity metadata is the empty + // string, parser metadata is "not-parser-inserted", credentials mode is + // "same-origin", referrer policy is the empty string, and fetch priority is + // "auto". + options = new ScriptFetchOptions( + mozilla::CORS_NONE, /* aNonce = */ u""_ns, RequestPriority::Auto, + ParserMetadata::NotParserInserted, principal, nullptr); + referrerPolicy = document->GetReferrerPolicy(); + baseURL = document->GetDocBaseURI(); + } + + context->mIsInline = false; + context->mScriptMode = ScriptLoadContext::ScriptMode::eAsync; + + RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest( + aURI, referrerPolicy, options, SRIMetadata(), baseURL, context, true, + /* is top level */ true, /* is dynamic import */ + this, ModuleLoadRequest::NewVisitedSetForTopLevelImport(aURI), nullptr); + + request->SetDynamicImport(aMaybeActiveScript, aSpecifier, aPromise); + request->NoCacheEntryFound(); + + return request.forget(); +} + +ModuleLoader::~ModuleLoader() { + LOG(("ModuleLoader::~ModuleLoader %p", this)); + mLoader = nullptr; +} + +#undef LOG +#undef LOG_ENABLED + +} // namespace mozilla::dom |