ScriptModuleLoader.cpp [plain text]
#include "config.h"
#include "ScriptModuleLoader.h"
#include "CachedModuleScriptLoader.h"
#include "CachedScript.h"
#include "CachedScriptFetcher.h"
#include "Document.h"
#include "Frame.h"
#include "JSDOMBinding.h"
#include "JSDOMPromiseDeferred.h"
#include "LoadableModuleScript.h"
#include "MIMETypeRegistry.h"
#include "ModuleFetchFailureKind.h"
#include "ModuleFetchParameters.h"
#include "ScriptController.h"
#include "ScriptSourceCode.h"
#include "SubresourceIntegrity.h"
#include "WebCoreJSClientData.h"
#include <JavaScriptCore/Completion.h>
#include <JavaScriptCore/JSInternalPromise.h>
#include <JavaScriptCore/JSModuleRecord.h>
#include <JavaScriptCore/JSScriptFetchParameters.h>
#include <JavaScriptCore/JSScriptFetcher.h>
#include <JavaScriptCore/JSSourceCode.h>
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/Symbol.h>
namespace WebCore {
ScriptModuleLoader::ScriptModuleLoader(Document& document)
: m_document(document)
{
}
ScriptModuleLoader::~ScriptModuleLoader()
{
for (auto& loader : m_loaders)
loader->clearClient();
}
static bool isRootModule(JSC::JSValue importerModuleKey)
{
return importerModuleKey.isSymbol() || importerModuleKey.isUndefined();
}
static Expected<URL, ASCIILiteral> resolveModuleSpecifier(Document& document, const String& specifier, const URL& baseURL)
{
URL absoluteURL(URL(), specifier);
if (absoluteURL.isValid())
return absoluteURL;
if (!specifier.startsWith('/') && !specifier.startsWith("./") && !specifier.startsWith("../"))
return makeUnexpected("Module specifier does not start with \"/\", \"./\", or \"../\"."_s);
auto result = document.completeURL(specifier, baseURL);
if (!result.isValid())
return makeUnexpected("Module name does not resolve to a valid URL."_s);
return result;
}
JSC::Identifier ScriptModuleLoader::resolve(JSC::JSGlobalObject* jsGlobalObject, JSC::JSModuleLoader*, JSC::JSValue moduleNameValue, JSC::JSValue importerModuleKey, JSC::JSValue)
{
JSC::VM& vm = jsGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
if (moduleNameValue.isSymbol())
return JSC::Identifier::fromUid(asSymbol(moduleNameValue)->privateName());
if (!moduleNameValue.isString()) {
JSC::throwTypeError(jsGlobalObject, scope, "Importer module key is not a Symbol or a String."_s);
return { };
}
String specifier = asString(moduleNameValue)->value(jsGlobalObject);
RETURN_IF_EXCEPTION(scope, { });
URL baseURL;
if (isRootModule(importerModuleKey))
baseURL = m_document.baseURL();
else {
ASSERT(importerModuleKey.isString());
URL importerModuleRequestURL(URL(), asString(importerModuleKey)->value(jsGlobalObject));
ASSERT_WITH_MESSAGE(importerModuleRequestURL.isValid(), "Invalid module referrer never starts importing dependent modules.");
auto iterator = m_requestURLToResponseURLMap.find(importerModuleRequestURL);
ASSERT_WITH_MESSAGE(iterator != m_requestURLToResponseURLMap.end(), "Module referrer must register itself to the map before starting importing dependent modules.");
baseURL = iterator->value;
}
auto result = resolveModuleSpecifier(m_document, specifier, baseURL);
if (!result) {
JSC::throwTypeError(jsGlobalObject, scope, result.error());
return { };
}
return JSC::Identifier::fromString(vm, result->string());
}
static void rejectToPropagateNetworkError(DeferredPromise& deferred, ModuleFetchFailureKind failureKind, ASCIILiteral message)
{
deferred.rejectWithCallback([&] (JSDOMGlobalObject& jsGlobalObject) {
JSC::VM& vm = jsGlobalObject.vm();
auto* error = JSC::createTypeError(&jsGlobalObject, message);
ASSERT(error);
error->putDirect(vm, static_cast<JSVMClientData&>(*vm.clientData).builtinNames().failureKindPrivateName(), JSC::jsNumber(static_cast<int32_t>(failureKind)));
return error;
});
}
JSC::JSInternalPromise* ScriptModuleLoader::fetch(JSC::JSGlobalObject* jsGlobalObject, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSValue parameters, JSC::JSValue scriptFetcher)
{
JSC::VM& vm = jsGlobalObject->vm();
ASSERT(JSC::jsDynamicCast<JSC::JSScriptFetcher*>(vm, scriptFetcher));
auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject);
auto* jsPromise = JSC::JSInternalPromise::create(vm, globalObject.internalPromiseStructure());
RELEASE_ASSERT(jsPromise);
auto deferred = DeferredPromise::create(globalObject, *jsPromise);
if (moduleKeyValue.isSymbol()) {
deferred->reject(TypeError, "Symbol module key should be already fulfilled with the inlined resource."_s);
return jsPromise;
}
if (!moduleKeyValue.isString()) {
deferred->reject(TypeError, "Module key is not Symbol or String."_s);
return jsPromise;
}
URL completedURL(URL(), asString(moduleKeyValue)->value(jsGlobalObject));
if (!completedURL.isValid()) {
deferred->reject(TypeError, "Module key is a valid URL."_s);
return jsPromise;
}
RefPtr<ModuleFetchParameters> topLevelFetchParameters;
if (auto* scriptFetchParameters = JSC::jsDynamicCast<JSC::JSScriptFetchParameters*>(vm, parameters))
topLevelFetchParameters = static_cast<ModuleFetchParameters*>(&scriptFetchParameters->parameters());
auto loader = CachedModuleScriptLoader::create(*this, deferred.get(), *static_cast<CachedScriptFetcher*>(JSC::jsCast<JSC::JSScriptFetcher*>(scriptFetcher)->fetcher()), WTFMove(topLevelFetchParameters));
m_loaders.add(loader.copyRef());
if (!loader->load(m_document, completedURL)) {
loader->clearClient();
m_loaders.remove(WTFMove(loader));
rejectToPropagateNetworkError(deferred.get(), ModuleFetchFailureKind::WasErrored, "Importing a module script failed."_s);
return jsPromise;
}
return jsPromise;
}
URL ScriptModuleLoader::moduleURL(JSC::JSGlobalObject& jsGlobalObject, JSC::JSValue moduleKeyValue)
{
if (moduleKeyValue.isSymbol())
return m_document.url();
ASSERT(moduleKeyValue.isString());
return URL(URL(), asString(moduleKeyValue)->value(&jsGlobalObject));
}
JSC::JSValue ScriptModuleLoader::evaluate(JSC::JSGlobalObject* jsGlobalObject, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSValue moduleRecordValue, JSC::JSValue)
{
JSC::VM& vm = jsGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* moduleRecord = JSC::jsDynamicCast<JSC::JSModuleRecord*>(vm, moduleRecordValue);
if (!moduleRecord)
return JSC::jsUndefined();
URL sourceURL = moduleURL(*jsGlobalObject, moduleKeyValue);
if (!sourceURL.isValid())
return JSC::throwTypeError(jsGlobalObject, scope, "Module key is an invalid URL."_s);
if (auto* frame = m_document.frame())
return frame->script().evaluateModule(sourceURL, *moduleRecord);
return JSC::jsUndefined();
}
static JSC::JSInternalPromise* rejectPromise(JSDOMGlobalObject& globalObject, ExceptionCode ec, ASCIILiteral message)
{
auto* jsPromise = JSC::JSInternalPromise::create(globalObject.vm(), globalObject.internalPromiseStructure());
RELEASE_ASSERT(jsPromise);
auto deferred = DeferredPromise::create(globalObject, *jsPromise);
deferred->reject(ec, WTFMove(message));
return jsPromise;
}
JSC::JSInternalPromise* ScriptModuleLoader::importModule(JSC::JSGlobalObject* jsGlobalObject, JSC::JSModuleLoader*, JSC::JSString* moduleName, JSC::JSValue parameters, const JSC::SourceOrigin& sourceOrigin)
{
JSC::VM& vm = jsGlobalObject->vm();
auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject);
URL baseURL;
RefPtr<JSC::ScriptFetcher> scriptFetcher;
if (sourceOrigin.isNull()) {
baseURL = m_document.baseURL();
scriptFetcher = CachedScriptFetcher::create(m_document.charset());
} else {
baseURL = URL(URL(), sourceOrigin.string());
if (!baseURL.isValid())
return rejectPromise(globalObject, TypeError, "Importer module key is not a Symbol or a String."_s);
if (sourceOrigin.fetcher())
scriptFetcher = sourceOrigin.fetcher();
else
scriptFetcher = CachedScriptFetcher::create(m_document.charset());
}
ASSERT(baseURL.isValid());
ASSERT(scriptFetcher);
auto specifier = moduleName->value(jsGlobalObject);
auto result = resolveModuleSpecifier(m_document, specifier, baseURL);
if (!result)
return rejectPromise(globalObject, TypeError, result.error());
return JSC::importModule(jsGlobalObject, JSC::Identifier::fromString(vm, result->string()), parameters, JSC::JSScriptFetcher::create(vm, WTFMove(scriptFetcher) ));
}
JSC::JSObject* ScriptModuleLoader::createImportMetaProperties(JSC::JSGlobalObject* jsGlobalObject, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSModuleRecord*, JSC::JSValue)
{
auto& vm = jsGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
URL sourceURL = moduleURL(*jsGlobalObject, moduleKeyValue);
ASSERT(sourceURL.isValid());
auto* metaProperties = JSC::constructEmptyObject(vm, jsGlobalObject->nullPrototypeObjectStructure());
RETURN_IF_EXCEPTION(scope, nullptr);
metaProperties->putDirect(vm, JSC::Identifier::fromString(vm, "url"), JSC::jsString(vm, sourceURL.string()));
RETURN_IF_EXCEPTION(scope, nullptr);
return metaProperties;
}
void ScriptModuleLoader::notifyFinished(CachedModuleScriptLoader& loader, RefPtr<DeferredPromise> promise)
{
URL sourceURL = loader.sourceURL();
if (!m_loaders.remove(&loader))
return;
loader.clearClient();
auto& cachedScript = *loader.cachedScript();
if (cachedScript.resourceError().isAccessControl()) {
promise->reject(TypeError, "Cross-origin script load denied by Cross-Origin Resource Sharing policy."_s);
return;
}
if (cachedScript.errorOccurred()) {
rejectToPropagateNetworkError(*promise, ModuleFetchFailureKind::WasErrored, "Importing a module script failed."_s);
return;
}
if (cachedScript.wasCanceled()) {
rejectToPropagateNetworkError(*promise, ModuleFetchFailureKind::WasCanceled, "Importing a module script is canceled."_s);
return;
}
if (!MIMETypeRegistry::isSupportedJavaScriptMIMEType(cachedScript.response().mimeType())) {
promise->reject(TypeError, makeString("'", cachedScript.response().mimeType(), "' is not a valid JavaScript MIME type."));
return;
}
if (auto* parameters = loader.parameters()) {
if (!matchIntegrityMetadata(cachedScript, parameters->integrity())) {
promise->reject(TypeError, makeString("Cannot load script ", integrityMismatchDescription(cachedScript, parameters->integrity())));
return;
}
}
m_requestURLToResponseURLMap.add(WTFMove(sourceURL), cachedScript.response().url());
promise->resolveWithCallback([&] (JSDOMGlobalObject& jsGlobalObject) {
return JSC::JSSourceCode::create(jsGlobalObject.vm(),
JSC::SourceCode { ScriptSourceCode { &cachedScript, JSC::SourceProviderSourceType::Module, loader.scriptFetcher() }.jsSourceCode() });
});
}
}