Source code

Revision control

Copy as Markdown

Other Tools

/* 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/StaticPrefs_dom.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<ScriptLoader*>(mLoader.get());
}
bool ModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) {
if (!GetScriptLoader()->GetDocument()) {
*aRvOut = NS_ERROR_NULL_POINTER;
return false;
}
nsCOMPtr<nsIPrincipal> 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, Nothing());
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);
// Step 1. Disallow further import maps given settings object.
if (!aRequest->GetScriptLoadContext()->IsPreload() &&
!StaticPrefs::dom_multiple_import_maps_enabled()) {
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() || aRequest->IsDynamicImport()) {
if (aRequest->GetScriptLoadContext()->mIsInline &&
aRequest->GetScriptLoadContext()->GetParserCreated() ==
NOT_FROM_PARSER) {
// 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<JSObject*> aGlobal, JS::CompileOptions& aOptions,
ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) {
if (!nsJSUtils::IsScriptable(aGlobal)) {
return NS_ERROR_FAILURE;
}
switch (aRequest->mModuleType) {
case JS::ModuleType::Unknown:
MOZ_CRASH("Unexpected module type");
case JS::ModuleType::JavaScriptOrWasm:
return CompileJavaScriptOrWasmModule(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:
case JS::ModuleType::Text:
MOZ_CRASH("Unexpected module type");
}
MOZ_CRASH("Unhandled module type");
}
nsresult ModuleLoader::CompileJavaScriptOrWasmModule(
JSContext* aCx, JS::CompileOptions& aOptions, ModuleLoadRequest* aRequest,
JS::MutableHandle<JSObject*> aModuleOut) {
GetScriptLoader()->CalculateCacheFlag(aRequest);
#ifdef NIGHTLY_BUILD
if (aRequest->HasWasmMimeTypeEssence()) {
MOZ_ASSERT(aRequest->IsWasmBytes());
auto* wasmModule =
JS::CompileWasmModule(aCx, aOptions, aRequest->WasmBytes());
if (!wasmModule) {
return NS_ERROR_FAILURE;
}
aModuleOut.set(wasmModule);
return NS_OK;
}
#endif
MOZ_ASSERT(!aRequest->IsWasmBytes());
if (aRequest->IsCachedStencil()) {
JS::InstantiateOptions instantiateOptions(aOptions);
RefPtr<JS::Stencil> 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<JS::Stencil> 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<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->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<JSObject*> 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<JSObject*> 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);
JS::Rooted<JSObject*> cssModule(aCx, nullptr);
ErrorResult error;
auto compile = [&](auto& source) {
using T = decltype(source);
static_assert(std::is_same_v<T, JS::SourceText<char16_t>&> ||
std::is_same_v<T, JS::SourceText<Utf8Unit>&>);
nsCOMPtr<nsPIDOMWindowInner> window =
do_QueryInterface(aRequest->GetGlobalObject());
if (!window) {
error.ThrowNotSupportedError(
"CSS module scripts not supported when there is no window");
return;
}
Document* constructorDocument = window->GetExtantDoc();
if (!constructorDocument) {
error.ThrowNotSupportedError(
"CSS module scripts 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
dom::CSSStyleSheetInit options;
RefPtr<StyleSheet> 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
if constexpr (std::is_same_v<T, JS::SourceText<mozilla::Utf8Unit>&>) {
nsDependentCSubstring text(source.get(), source.length());
sheet->ReplaceSync(text, error);
} else if constexpr (std::is_same_v<T, JS::SourceText<char16_t>&>) {
nsDependentSubstring text(source.get(), source.length());
sheet->ReplaceSync(NS_ConvertUTF16toUTF8(text), error);
}
if (error.Failed()) {
return;
}
JS::Rooted<JS::Value> 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<ModuleLoadRequest> ModuleLoader::CreateTopLevel(
nsIURI* aURI, nsIScriptElement* aElement, ReferrerPolicy aReferrerPolicy,
ScriptFetchOptions* aFetchOptions, const SRIMetadata& aIntegrity,
nsIURI* aReferrer, ScriptLoadContext* aContext,
ScriptLoadRequestType aRequestType) {
RefPtr<ModuleLoadRequest> 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<ModuleLoadRequest> ModuleLoader::CreateRequest(
JSContext* aCx, nsIURI* aURI, JS::Handle<JSObject*> aModuleRequest,
JS::Handle<JS::Value> aHostDefined, JS::Handle<JS::Value> aPayload,
bool aIsDynamicImport, ScriptFetchOptions* aOptions,
ReferrerPolicy aReferrerPolicy, nsIURI* aBaseURL,
const SRIMetadata& aSriMetadata) {
RefPtr<ScriptLoadContext> 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<ModuleLoadRequest*>(aHostDefined.toPrivate());
MOZ_ASSERT(root);
LoadContextBase* loadContext = root->mLoadContext;
context->mScriptMode = loadContext->AsWindowContext()->mScriptMode;
if (loadContext->AsWindowContext()->mIsPreload) {
context->mIsPreload = true;
}
kind = ModuleLoadRequest::Kind::StaticImport;
}
JS::ModuleType moduleType = GetModuleRequestType(aCx, aModuleRequest);
RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
moduleType, aSriMetadata, aBaseURL, context, kind, this, root);
GetScriptLoader()->TryUseCache(aReferrerPolicy, aOptions, aURI, request);
return request.forget();
}
already_AddRefed<ScriptFetchOptions>
ModuleLoader::CreateDefaultScriptFetchOptions() {
RefPtr<ScriptFetchOptions> options = ScriptFetchOptions::CreateDefault();
nsCOMPtr<nsIPrincipal> principal = GetGlobalObject()->PrincipalOrNull();
options->SetTriggeringPrincipal(principal);
return options.forget();
}
nsIURI* ModuleLoader::GetClientReferrerURI() {
Document* document = GetScriptLoader()->GetDocument();
#ifdef DEBUG
nsCOMPtr<nsIPrincipal> 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