| 1 | /* |
| 2 | * Copyright (C) 2015-2017 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 23 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "ScriptModuleLoader.h" |
| 28 | |
| 29 | #include "CachedModuleScriptLoader.h" |
| 30 | #include "CachedScript.h" |
| 31 | #include "CachedScriptFetcher.h" |
| 32 | #include "Document.h" |
| 33 | #include "Frame.h" |
| 34 | #include "JSDOMBinding.h" |
| 35 | #include "LoadableModuleScript.h" |
| 36 | #include "MIMETypeRegistry.h" |
| 37 | #include "ModuleFetchFailureKind.h" |
| 38 | #include "ModuleFetchParameters.h" |
| 39 | #include "ScriptController.h" |
| 40 | #include "ScriptSourceCode.h" |
| 41 | #include "SubresourceIntegrity.h" |
| 42 | #include "WebCoreJSClientData.h" |
| 43 | #include <JavaScriptCore/Completion.h> |
| 44 | #include <JavaScriptCore/JSInternalPromise.h> |
| 45 | #include <JavaScriptCore/JSInternalPromiseDeferred.h> |
| 46 | #include <JavaScriptCore/JSModuleRecord.h> |
| 47 | #include <JavaScriptCore/JSScriptFetchParameters.h> |
| 48 | #include <JavaScriptCore/JSScriptFetcher.h> |
| 49 | #include <JavaScriptCore/JSSourceCode.h> |
| 50 | #include <JavaScriptCore/JSString.h> |
| 51 | #include <JavaScriptCore/Symbol.h> |
| 52 | |
| 53 | namespace WebCore { |
| 54 | |
| 55 | ScriptModuleLoader::ScriptModuleLoader(Document& document) |
| 56 | : m_document(document) |
| 57 | { |
| 58 | } |
| 59 | |
| 60 | ScriptModuleLoader::~ScriptModuleLoader() |
| 61 | { |
| 62 | for (auto& loader : m_loaders) |
| 63 | loader->clearClient(); |
| 64 | } |
| 65 | |
| 66 | static bool isRootModule(JSC::JSValue importerModuleKey) |
| 67 | { |
| 68 | return importerModuleKey.isSymbol() || importerModuleKey.isUndefined(); |
| 69 | } |
| 70 | |
| 71 | static Expected<URL, ASCIILiteral> resolveModuleSpecifier(Document& document, const String& specifier, const URL& baseURL) |
| 72 | { |
| 73 | // https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier |
| 74 | |
| 75 | URL absoluteURL(URL(), specifier); |
| 76 | if (absoluteURL.isValid()) |
| 77 | return absoluteURL; |
| 78 | |
| 79 | if (!specifier.startsWith('/') && !specifier.startsWith("./" ) && !specifier.startsWith("../" )) |
| 80 | return makeUnexpected("Module specifier does not start with \"/\", \"./\", or \"../\"."_s ); |
| 81 | |
| 82 | auto result = document.completeURL(specifier, baseURL); |
| 83 | if (!result.isValid()) |
| 84 | return makeUnexpected("Module name does not resolve to a valid URL."_s ); |
| 85 | return result; |
| 86 | } |
| 87 | |
| 88 | JSC::Identifier ScriptModuleLoader::resolve(JSC::JSGlobalObject*, JSC::ExecState* exec, JSC::JSModuleLoader*, JSC::JSValue moduleNameValue, JSC::JSValue importerModuleKey, JSC::JSValue) |
| 89 | { |
| 90 | JSC::VM& vm = exec->vm(); |
| 91 | auto scope = DECLARE_THROW_SCOPE(vm); |
| 92 | |
| 93 | // We use a Symbol as a special purpose; It means this module is an inline module. |
| 94 | // So there is no correct URL to retrieve the module source code. If the module name |
| 95 | // value is a Symbol, it is used directly as a module key. |
| 96 | if (moduleNameValue.isSymbol()) |
| 97 | return JSC::Identifier::fromUid(asSymbol(moduleNameValue)->privateName()); |
| 98 | |
| 99 | if (!moduleNameValue.isString()) { |
| 100 | JSC::throwTypeError(exec, scope, "Importer module key is not a Symbol or a String."_s ); |
| 101 | return { }; |
| 102 | } |
| 103 | |
| 104 | String specifier = asString(moduleNameValue)->value(exec); |
| 105 | RETURN_IF_EXCEPTION(scope, { }); |
| 106 | |
| 107 | URL baseURL; |
| 108 | if (isRootModule(importerModuleKey)) |
| 109 | baseURL = m_document.baseURL(); |
| 110 | else { |
| 111 | ASSERT(importerModuleKey.isString()); |
| 112 | URL importerModuleRequestURL(URL(), asString(importerModuleKey)->value(exec)); |
| 113 | ASSERT_WITH_MESSAGE(importerModuleRequestURL.isValid(), "Invalid module referrer never starts importing dependent modules." ); |
| 114 | |
| 115 | auto iterator = m_requestURLToResponseURLMap.find(importerModuleRequestURL); |
| 116 | ASSERT_WITH_MESSAGE(iterator != m_requestURLToResponseURLMap.end(), "Module referrer must register itself to the map before starting importing dependent modules." ); |
| 117 | baseURL = iterator->value; |
| 118 | } |
| 119 | |
| 120 | auto result = resolveModuleSpecifier(m_document, specifier, baseURL); |
| 121 | if (!result) { |
| 122 | JSC::throwTypeError(exec, scope, result.error()); |
| 123 | return { }; |
| 124 | } |
| 125 | |
| 126 | return JSC::Identifier::fromString(&vm, result->string()); |
| 127 | } |
| 128 | |
| 129 | static void rejectToPropagateNetworkError(DeferredPromise& deferred, ModuleFetchFailureKind failureKind, ASCIILiteral message) |
| 130 | { |
| 131 | deferred.rejectWithCallback([&] (JSC::ExecState& state, JSDOMGlobalObject&) { |
| 132 | // We annotate exception with special private symbol. It allows us to distinguish these errors from the user thrown ones. |
| 133 | JSC::VM& vm = state.vm(); |
| 134 | // FIXME: Propagate more descriptive error. |
| 135 | // https://bugs.webkit.org/show_bug.cgi?id=167553 |
| 136 | auto* error = JSC::createTypeError(&state, message); |
| 137 | ASSERT(error); |
| 138 | error->putDirect(vm, static_cast<JSVMClientData&>(*vm.clientData).builtinNames().failureKindPrivateName(), JSC::jsNumber(static_cast<int32_t>(failureKind))); |
| 139 | return error; |
| 140 | }); |
| 141 | } |
| 142 | |
| 143 | JSC::JSInternalPromise* ScriptModuleLoader::fetch(JSC::JSGlobalObject* jsGlobalObject, JSC::ExecState* exec, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSValue parameters, JSC::JSValue scriptFetcher) |
| 144 | { |
| 145 | JSC::VM& vm = exec->vm(); |
| 146 | ASSERT(JSC::jsDynamicCast<JSC::JSScriptFetcher*>(vm, scriptFetcher)); |
| 147 | |
| 148 | auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject); |
| 149 | auto* jsPromise = JSC::JSInternalPromiseDeferred::tryCreate(exec, &globalObject); |
| 150 | RELEASE_ASSERT(jsPromise); |
| 151 | auto deferred = DeferredPromise::create(globalObject, *jsPromise); |
| 152 | if (moduleKeyValue.isSymbol()) { |
| 153 | deferred->reject(TypeError, "Symbol module key should be already fulfilled with the inlined resource."_s ); |
| 154 | return jsPromise->promise(); |
| 155 | } |
| 156 | |
| 157 | if (!moduleKeyValue.isString()) { |
| 158 | deferred->reject(TypeError, "Module key is not Symbol or String."_s ); |
| 159 | return jsPromise->promise(); |
| 160 | } |
| 161 | |
| 162 | // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script |
| 163 | |
| 164 | URL completedURL(URL(), asString(moduleKeyValue)->value(exec)); |
| 165 | if (!completedURL.isValid()) { |
| 166 | deferred->reject(TypeError, "Module key is a valid URL."_s ); |
| 167 | return jsPromise->promise(); |
| 168 | } |
| 169 | |
| 170 | RefPtr<ModuleFetchParameters> topLevelFetchParameters; |
| 171 | if (auto* scriptFetchParameters = JSC::jsDynamicCast<JSC::JSScriptFetchParameters*>(vm, parameters)) |
| 172 | topLevelFetchParameters = static_cast<ModuleFetchParameters*>(&scriptFetchParameters->parameters()); |
| 173 | |
| 174 | auto loader = CachedModuleScriptLoader::create(*this, deferred.get(), *static_cast<CachedScriptFetcher*>(JSC::jsCast<JSC::JSScriptFetcher*>(scriptFetcher)->fetcher()), WTFMove(topLevelFetchParameters)); |
| 175 | m_loaders.add(loader.copyRef()); |
| 176 | if (!loader->load(m_document, completedURL)) { |
| 177 | loader->clearClient(); |
| 178 | m_loaders.remove(WTFMove(loader)); |
| 179 | rejectToPropagateNetworkError(deferred.get(), ModuleFetchFailureKind::WasErrored, "Importing a module script failed."_s ); |
| 180 | return jsPromise->promise(); |
| 181 | } |
| 182 | |
| 183 | return jsPromise->promise(); |
| 184 | } |
| 185 | |
| 186 | URL ScriptModuleLoader::moduleURL(JSC::ExecState& state, JSC::JSValue moduleKeyValue) |
| 187 | { |
| 188 | if (moduleKeyValue.isSymbol()) |
| 189 | return m_document.url(); |
| 190 | |
| 191 | ASSERT(moduleKeyValue.isString()); |
| 192 | return URL(URL(), asString(moduleKeyValue)->value(&state)); |
| 193 | } |
| 194 | |
| 195 | JSC::JSValue ScriptModuleLoader::evaluate(JSC::JSGlobalObject*, JSC::ExecState* exec, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSValue moduleRecordValue, JSC::JSValue) |
| 196 | { |
| 197 | JSC::VM& vm = exec->vm(); |
| 198 | auto scope = DECLARE_THROW_SCOPE(vm); |
| 199 | |
| 200 | // FIXME: Currently, we only support JSModuleRecord. |
| 201 | // Once the reflective part of the module loader is supported, we will handle arbitrary values. |
| 202 | // https://whatwg.github.io/loader/#registry-prototype-provide |
| 203 | auto* moduleRecord = JSC::jsDynamicCast<JSC::JSModuleRecord*>(vm, moduleRecordValue); |
| 204 | if (!moduleRecord) |
| 205 | return JSC::jsUndefined(); |
| 206 | |
| 207 | URL sourceURL = moduleURL(*exec, moduleKeyValue); |
| 208 | if (!sourceURL.isValid()) |
| 209 | return JSC::throwTypeError(exec, scope, "Module key is an invalid URL."_s ); |
| 210 | |
| 211 | if (auto* frame = m_document.frame()) |
| 212 | return frame->script().evaluateModule(sourceURL, *moduleRecord); |
| 213 | return JSC::jsUndefined(); |
| 214 | } |
| 215 | |
| 216 | static JSC::JSInternalPromise* rejectPromise(JSC::ExecState& state, JSDOMGlobalObject& globalObject, ExceptionCode ec, ASCIILiteral message) |
| 217 | { |
| 218 | auto* jsPromise = JSC::JSInternalPromiseDeferred::tryCreate(&state, &globalObject); |
| 219 | RELEASE_ASSERT(jsPromise); |
| 220 | auto deferred = DeferredPromise::create(globalObject, *jsPromise); |
| 221 | deferred->reject(ec, WTFMove(message)); |
| 222 | return jsPromise->promise(); |
| 223 | } |
| 224 | |
| 225 | JSC::JSInternalPromise* ScriptModuleLoader::importModule(JSC::JSGlobalObject* jsGlobalObject, JSC::ExecState* exec, JSC::JSModuleLoader*, JSC::JSString* moduleName, JSC::JSValue parameters, const JSC::SourceOrigin& sourceOrigin) |
| 226 | { |
| 227 | auto& state = *exec; |
| 228 | JSC::VM& vm = exec->vm(); |
| 229 | auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject); |
| 230 | |
| 231 | // If SourceOrigin and/or CachedScriptFetcher is null, we import the module with the default fetcher. |
| 232 | // SourceOrigin can be null if the source code is not coupled with the script file. |
| 233 | // The examples, |
| 234 | // 1. The code evaluated by the inspector. |
| 235 | // 2. The other unusual code execution like the evaluation through the NPAPI. |
| 236 | // 3. The code from injected bundle's script. |
| 237 | // 4. The code from extension script. |
| 238 | URL baseURL; |
| 239 | RefPtr<JSC::ScriptFetcher> scriptFetcher; |
| 240 | if (sourceOrigin.isNull()) { |
| 241 | baseURL = m_document.baseURL(); |
| 242 | scriptFetcher = CachedScriptFetcher::create(m_document.charset()); |
| 243 | } else { |
| 244 | baseURL = URL(URL(), sourceOrigin.string()); |
| 245 | if (!baseURL.isValid()) |
| 246 | return rejectPromise(state, globalObject, TypeError, "Importer module key is not a Symbol or a String."_s ); |
| 247 | |
| 248 | if (sourceOrigin.fetcher()) |
| 249 | scriptFetcher = sourceOrigin.fetcher(); |
| 250 | else |
| 251 | scriptFetcher = CachedScriptFetcher::create(m_document.charset()); |
| 252 | } |
| 253 | ASSERT(baseURL.isValid()); |
| 254 | ASSERT(scriptFetcher); |
| 255 | |
| 256 | auto specifier = moduleName->value(exec); |
| 257 | auto result = resolveModuleSpecifier(m_document, specifier, baseURL); |
| 258 | if (!result) |
| 259 | return rejectPromise(state, globalObject, TypeError, result.error()); |
| 260 | |
| 261 | return JSC::importModule(exec, JSC::Identifier::fromString(&vm, result->string()), parameters, JSC::JSScriptFetcher::create(vm, WTFMove(scriptFetcher) )); |
| 262 | } |
| 263 | |
| 264 | JSC::JSObject* ScriptModuleLoader::createImportMetaProperties(JSC::JSGlobalObject* globalObject, JSC::ExecState* exec, JSC::JSModuleLoader*, JSC::JSValue moduleKeyValue, JSC::JSModuleRecord*, JSC::JSValue) |
| 265 | { |
| 266 | auto& vm = exec->vm(); |
| 267 | auto scope = DECLARE_THROW_SCOPE(vm); |
| 268 | |
| 269 | URL sourceURL = moduleURL(*exec, moduleKeyValue); |
| 270 | ASSERT(sourceURL.isValid()); |
| 271 | |
| 272 | auto* metaProperties = JSC::constructEmptyObject(exec, globalObject->nullPrototypeObjectStructure()); |
| 273 | RETURN_IF_EXCEPTION(scope, nullptr); |
| 274 | |
| 275 | metaProperties->putDirect(vm, JSC::Identifier::fromString(&vm, "url" ), JSC::jsString(&vm, sourceURL.string())); |
| 276 | RETURN_IF_EXCEPTION(scope, nullptr); |
| 277 | |
| 278 | return metaProperties; |
| 279 | } |
| 280 | |
| 281 | void ScriptModuleLoader::notifyFinished(CachedModuleScriptLoader& loader, RefPtr<DeferredPromise> promise) |
| 282 | { |
| 283 | // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script |
| 284 | |
| 285 | if (!m_loaders.remove(&loader)) |
| 286 | return; |
| 287 | loader.clearClient(); |
| 288 | |
| 289 | auto& cachedScript = *loader.cachedScript(); |
| 290 | |
| 291 | if (cachedScript.resourceError().isAccessControl()) { |
| 292 | promise->reject(TypeError, "Cross-origin script load denied by Cross-Origin Resource Sharing policy."_s ); |
| 293 | return; |
| 294 | } |
| 295 | |
| 296 | if (cachedScript.errorOccurred()) { |
| 297 | rejectToPropagateNetworkError(*promise, ModuleFetchFailureKind::WasErrored, "Importing a module script failed."_s ); |
| 298 | return; |
| 299 | } |
| 300 | |
| 301 | if (cachedScript.wasCanceled()) { |
| 302 | rejectToPropagateNetworkError(*promise, ModuleFetchFailureKind::WasCanceled, "Importing a module script is canceled."_s ); |
| 303 | return; |
| 304 | } |
| 305 | |
| 306 | if (!MIMETypeRegistry::isSupportedJavaScriptMIMEType(cachedScript.response().mimeType())) { |
| 307 | // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script |
| 308 | // The result of extracting a MIME type from response's header list (ignoring parameters) is not a JavaScript MIME type. |
| 309 | // For historical reasons, fetching a classic script does not include MIME type checking. In contrast, module scripts will fail to load if they are not of a correct MIME type. |
| 310 | promise->reject(TypeError, makeString("'" , cachedScript.response().mimeType(), "' is not a valid JavaScript MIME type." )); |
| 311 | return; |
| 312 | } |
| 313 | |
| 314 | if (auto* parameters = loader.parameters()) { |
| 315 | if (!matchIntegrityMetadata(cachedScript, parameters->integrity())) { |
| 316 | promise->reject(TypeError, makeString("Cannot load script " , cachedScript.url().stringCenterEllipsizedToLength(), ". Failed integrity metadata check." )); |
| 317 | return; |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | m_requestURLToResponseURLMap.add(cachedScript.url(), cachedScript.response().url()); |
| 322 | promise->resolveWithCallback([&] (JSC::ExecState& state, JSDOMGlobalObject&) { |
| 323 | return JSC::JSSourceCode::create(state.vm(), |
| 324 | JSC::SourceCode { ScriptSourceCode { &cachedScript, JSC::SourceProviderSourceType::Module, loader.scriptFetcher() }.jsSourceCode() }); |
| 325 | }); |
| 326 | } |
| 327 | |
| 328 | } |
| 329 | |