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 | |