/* -*- 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 "ModuleLoader.h" #include "GeckoProfiler.h" #include "ScriptLoader.h" #include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions #include "js/ContextOptions.h" // JS::ContextOptionsRef #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/experimental/JSStencil.h" // JS::Stencil, JS::CompileModuleScriptToStencil, JS::InstantiateModuleStencil #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/loader/LoadedScript.h" #include "js/loader/ModuleLoadRequest.h" #include "js/loader/ModuleLoaderBase.h" #include "js/loader/ScriptLoadRequest.h" #include "jsapi.h" #include "mozilla/Assertions.h" #include "mozilla/CycleCollectedJSContext.h" #include "mozilla/LoadInfo.h" #include "mozilla/Maybe.h" #include "mozilla/StyleSheet.h" #include "mozilla/StyleSheetInlines.h" #include "mozilla/dom/AutoEntryScript.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/RequestBinding.h" #include "nsContentSecurityManager.h" #include "nsError.h" #include "nsIContent.h" #include "nsIPrincipal.h" #include "nsJSUtils.h" #include "xpcpublic.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(mLoader.get()); } bool ModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) { if (!GetScriptLoader()->GetDocument()) { *aRvOut = NS_ERROR_NULL_POINTER; return false; } nsCOMPtr principal = aRequest->TriggeringPrincipal(); if (BasePrincipal::Cast(principal)->ContentScriptAddonPolicy()) { // To prevent dynamic code execution, content scripts can only // load moz-extension URLs. if (!aRequest->URI()->SchemeIs("moz-extension")) { *aRvOut = NS_ERROR_DOM_WEBEXT_CONTENT_SCRIPT_URI; return false; } } else { // If this document is sandboxed without 'allow-scripts', abort. if (GetScriptLoader()->GetDocument()->HasScriptsBlockedBySandbox()) { *aRvOut = NS_ERROR_CONTENT_BLOCKED; return false; } } if (LOG_ENABLED()) { nsAutoCString url; aRequest->URI()->GetAsciiSpec(url); LOG(("ScriptLoadRequest (%p): Start Module Load (url = %s)", aRequest, url.get())); } return true; } nsresult ModuleLoader::StartFetch(ModuleLoadRequest* aRequest) { if (aRequest->IsCachedStencil()) { GetScriptLoader()->EmulateNetworkEvents(aRequest); SetModuleFetchStarted(aRequest); return aRequest->OnFetchComplete(NS_OK); } // 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>( "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() || aRequest->IsDynamicImport()) { if (aRequest->GetScriptLoadContext()->mIsInline && aRequest->GetScriptLoadContext()->GetParserCreated() == NOT_FROM_PARSER) { // https://html.spec.whatwg.org/#prepare-the-script-element // Step 32.2. // type: "module": // 3.1. Queue an element task on the networking task source given // el to perform the following steps: // 1. Mark as ready el given result. // // Step 33. If ... el's type is "module": // ... // 3. Otherwise, if el is not parser-inserted: // 3. Set el's steps to run when the result is ready to the // following: // ... // 2.1. Execute the script element scripts[0]. 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 aGlobal, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleOut) { if (!nsJSUtils::IsScriptable(aGlobal)) { return NS_ERROR_FAILURE; } switch (aRequest->mModuleType) { case JS::ModuleType::Unknown: MOZ_CRASH("Unexpected module type"); case JS::ModuleType::JavaScript: return CompileJavaScriptModule(aCx, aOptions, aRequest, aModuleOut); case JS::ModuleType::JSON: return CompileJsonModule(aCx, aOptions, aRequest, aModuleOut); case JS::ModuleType::CSS: return CompileCssModule(aCx, aOptions, aRequest, aModuleOut); case JS::ModuleType::Bytes: MOZ_CRASH("Unexpected module type"); } MOZ_CRASH("Unhandled module type"); } nsresult ModuleLoader::CompileJavaScriptModule( JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleOut) { GetScriptLoader()->CalculateCacheFlag(aRequest); if (aRequest->IsCachedStencil()) { JS::InstantiateOptions instantiateOptions(aOptions); RefPtr stencil = aRequest->GetStencil(); aModuleOut.set( JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); if (!aModuleOut) { return NS_ERROR_FAILURE; } bool alreadyStarted; if (!JS::StartCollectingDelazifications(aCx, aModuleOut, stencil, alreadyStarted)) { return NS_ERROR_FAILURE; } (void)alreadyStarted; return NS_OK; } if (aRequest->GetScriptLoadContext()->mWasCompiledOMT) { JS::InstantiationStorage storage; RefPtr stencil = aRequest->GetScriptLoadContext()->StealOffThreadResult(aCx, &storage); if (!stencil) { return NS_ERROR_FAILURE; } aRequest->SetStencil(stencil); JS::InstantiateOptions instantiateOptions(aOptions); aModuleOut.set(JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil, &storage)); if (!aModuleOut) { return NS_ERROR_FAILURE; } if (aRequest->PassedConditionForEitherCache()) { bool alreadyStarted; if (!JS::StartCollectingDelazifications(aCx, aModuleOut, stencil, alreadyStarted)) { return NS_ERROR_FAILURE; } MOZ_ASSERT(!alreadyStarted); } GetScriptLoader()->TryCacheRequest(aRequest); return NS_OK; } RefPtr 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->IsSerializedStencil()); JS::DecodeOptions decodeOptions(aOptions); decodeOptions.borrowBuffer = true; JS::TranscodeRange range = aRequest->SerializedStencil(); 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; } aRequest->SetStencil(stencil); JS::InstantiateOptions instantiateOptions(aOptions); aModuleOut.set( JS::InstantiateModuleStencil(aCx, instantiateOptions, stencil)); if (!aModuleOut) { return NS_ERROR_FAILURE; } if (aRequest->PassedConditionForEitherCache()) { bool alreadyStarted; if (!JS::StartCollectingDelazifications(aCx, aModuleOut, stencil, alreadyStarted)) { return NS_ERROR_FAILURE; } MOZ_ASSERT(!alreadyStarted); } GetScriptLoader()->TryCacheRequest(aRequest); return NS_OK; } nsresult ModuleLoader::CompileJsonModule( JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleOut) { MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mWasCompiledOMT); MOZ_ASSERT(aRequest->IsTextSource()); ModuleLoader::MaybeSourceText maybeSource; nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, aRequest->mLoadContext.get()); NS_ENSURE_SUCCESS(rv, rv); auto compile = [&](auto& source) { return JS::CompileJsonModule(aCx, aOptions, source); }; auto* jsonModule = maybeSource.mapNonEmpty(compile); if (!jsonModule) { return NS_ERROR_FAILURE; } aModuleOut.set(jsonModule); return NS_OK; } nsresult ModuleLoader::CompileCssModule( JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest, JS::MutableHandle aModuleOut) { MOZ_ASSERT(!aRequest->GetScriptLoadContext()->mWasCompiledOMT); MOZ_ASSERT(mozilla::StaticPrefs::layout_css_module_scripts_enabled()); MOZ_ASSERT(aRequest->IsTextSource()); ModuleLoader::MaybeSourceText maybeSource; nsresult rv = aRequest->GetScriptSource(aCx, &maybeSource, aRequest->mLoadContext.get()); NS_ENSURE_SUCCESS(rv, rv); // https://html.spec.whatwg.org/#creating-a-css-module-script JS::Rooted cssModule(aCx, nullptr); ErrorResult error; auto compile = [&](auto& source) { using T = decltype(source); static_assert(std::is_same_v&> || std::is_same_v&>); nsCOMPtr window = do_QueryInterface(aRequest->GetGlobalObject()); if (!window) { error.ThrowNotSupportedError("Not supported when there is no document"); return; } Document* constructorDocument = window->GetExtantDoc(); if (!constructorDocument) { error.ThrowNotSupportedError("Not supported when there is no document"); return; } // 5. Let sheet be the result of running the steps to create a constructed // CSSStyleSheet // with an empty dictionary as the argument. // Note that according to the specification, the baseURL should be the // baseURL of the document, but that doesn't seem correct (see // https://github.com/whatwg/html/issues/11629). dom::CSSStyleSheetInit options; RefPtr sheet = StyleSheet::CreateConstructedSheet( *constructorDocument, aRequest->BaseURL(), options, error); if (error.Failed()) { return; } // 6. Run the steps to synchronously replace the rules of a CSSStyleSheet on // sheet given source. Ideally we wouldn't run this on the main thread for // large scripts, see https://bugzilla.mozilla.org/show_bug.cgi?id=1987143. if constexpr (std::is_same_v&>) { nsDependentCSubstring text(source.get(), source.length()); sheet->ReplaceSync(text, error); } else if constexpr (std::is_same_v&>) { nsDependentSubstring text(source.get(), source.length()); sheet->ReplaceSync(NS_ConvertUTF16toUTF8(text), error); } if (error.Failed()) { return; } JS::Rooted val(aCx, JS::NullValue()); if (!GetOrCreateDOMReflector(aCx, sheet, &val) || !val.isObject()) { if (!JS_IsExceptionPending(aCx)) { error.ThrowUnknownError("Internal error"); } return; } // Steps. 1 - 4 (re-ordered), 7, 8 cssModule.set(JS::CreateDefaultExportSyntheticModule(aCx, val)); }; maybeSource.mapNonEmpty(compile); if (!cssModule) { if (error.Failed()) { MOZ_ALWAYS_TRUE(error.MaybeSetPendingException(aCx)); } return NS_ERROR_FAILURE; } aModuleOut.set(cssModule); return NS_OK; } already_AddRefed ModuleLoader::CreateTopLevel( nsIURI* aURI, nsIScriptElement* aElement, ReferrerPolicy aReferrerPolicy, ScriptFetchOptions* aFetchOptions, const SRIMetadata& aIntegrity, nsIURI* aReferrer, ScriptLoadContext* aContext, ScriptLoadRequestType aRequestType) { RefPtr request = new ModuleLoadRequest( JS::ModuleType::JavaScript, aIntegrity, aReferrer, aContext, ModuleLoadRequest::Kind::TopLevel, this, nullptr); GetScriptLoader()->TryUseCache(aReferrerPolicy, aFetchOptions, aURI, request, aElement, aFetchOptions->mNonce, aRequestType); return request.forget(); } already_AddRefed ModuleLoader::CreateRequest( JSContext* aCx, nsIURI* aURI, JS::Handle aModuleRequest, JS::Handle aHostDefined, JS::Handle aPayload, bool aIsDynamicImport, ScriptFetchOptions* aOptions, ReferrerPolicy aReferrerPolicy, nsIURI* aBaseURL, const SRIMetadata& aSriMetadata) { RefPtr context = new ScriptLoadContext(); context->mIsInline = false; ModuleLoadRequest::Kind kind; ModuleLoadRequest* root = nullptr; if (aIsDynamicImport) { context->mScriptMode = ScriptLoadContext::ScriptMode::eAsync; kind = ModuleLoadRequest::Kind::DynamicImport; } else { MOZ_ASSERT(!aHostDefined.isUndefined()); root = static_cast(aHostDefined.toPrivate()); MOZ_ASSERT(root); LoadContextBase* loadContext = root->mLoadContext; context->mScriptMode = loadContext->AsWindowContext()->mScriptMode; kind = ModuleLoadRequest::Kind::StaticImport; } JS::ModuleType moduleType = GetModuleRequestType(aCx, aModuleRequest); RefPtr request = new ModuleLoadRequest( moduleType, aSriMetadata, aBaseURL, context, kind, this, root); GetScriptLoader()->TryUseCache(aReferrerPolicy, aOptions, aURI, request); return request.forget(); } already_AddRefed ModuleLoader::CreateDefaultScriptFetchOptions() { RefPtr options = ScriptFetchOptions::CreateDefault(); nsCOMPtr principal = GetGlobalObject()->PrincipalOrNull(); options->SetTriggeringPrincipal(principal); return options.forget(); } nsIURI* ModuleLoader::GetClientReferrerURI() { Document* document = GetScriptLoader()->GetDocument(); #ifdef DEBUG nsCOMPtr principal = GetGlobalObject()->PrincipalOrNull(); #endif // DEBUG MOZ_ASSERT_IF(GetKind() == WebExtension, BasePrincipal::Cast(principal)->ContentScriptAddonPolicy()); MOZ_ASSERT_IF(GetKind() == Normal, principal == document->NodePrincipal()); return document->GetDocBaseURI(); } ModuleLoader::~ModuleLoader() { LOG(("ModuleLoader::~ModuleLoader %p", this)); mLoader = nullptr; } #undef LOG #undef LOG_ENABLED } // namespace mozilla::dom