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
53namespace WebCore {
54
55ScriptModuleLoader::ScriptModuleLoader(Document& document)
56 : m_document(document)
57{
58}
59
60ScriptModuleLoader::~ScriptModuleLoader()
61{
62 for (auto& loader : m_loaders)
63 loader->clearClient();
64}
65
66static bool isRootModule(JSC::JSValue importerModuleKey)
67{
68 return importerModuleKey.isSymbol() || importerModuleKey.isUndefined();
69}
70
71static 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
88JSC::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
129static 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
143JSC::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
186URL 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
195JSC::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
216static 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
225JSC::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
264JSC::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
281void 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