| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h" |
| |
| #include "third_party/blink/renderer/bindings/core/v8/script_module.h" |
| #include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h" |
| #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h" |
| #include "third_party/blink/renderer/core/script/layered_api.h" |
| #include "third_party/blink/renderer/core/script/module_script.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_loading_log.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_policy.h" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| #include "v8/include/v8.h" |
| |
| namespace blink { |
| |
| void ModuleTreeLinker::Fetch( |
| const KURL& url, |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| mojom::RequestContextType destination, |
| const ScriptFetchOptions& options, |
| Modulator* modulator, |
| ModuleScriptCustomFetchType custom_fetch_type, |
| ModuleTreeLinkerRegistry* registry, |
| ModuleTreeClient* client) { |
| ModuleTreeLinker* fetcher = MakeGarbageCollected<ModuleTreeLinker>( |
| fetch_client_settings_object_fetcher, destination, modulator, |
| custom_fetch_type, registry, client); |
| registry->AddFetcher(fetcher); |
| fetcher->FetchRoot(url, options); |
| DCHECK(fetcher->IsFetching()); |
| } |
| |
| void ModuleTreeLinker::FetchDescendantsForInlineScript( |
| ModuleScript* module_script, |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| mojom::RequestContextType destination, |
| Modulator* modulator, |
| ModuleScriptCustomFetchType custom_fetch_type, |
| ModuleTreeLinkerRegistry* registry, |
| ModuleTreeClient* client) { |
| DCHECK(module_script); |
| ModuleTreeLinker* fetcher = MakeGarbageCollected<ModuleTreeLinker>( |
| fetch_client_settings_object_fetcher, destination, modulator, |
| custom_fetch_type, registry, client); |
| registry->AddFetcher(fetcher); |
| fetcher->FetchRootInline(module_script); |
| DCHECK(fetcher->IsFetching()); |
| } |
| |
| ModuleTreeLinker::ModuleTreeLinker( |
| ResourceFetcher* fetch_client_settings_object_fetcher, |
| mojom::RequestContextType destination, |
| Modulator* modulator, |
| ModuleScriptCustomFetchType custom_fetch_type, |
| ModuleTreeLinkerRegistry* registry, |
| ModuleTreeClient* client) |
| : fetch_client_settings_object_fetcher_( |
| fetch_client_settings_object_fetcher), |
| destination_(destination), |
| modulator_(modulator), |
| custom_fetch_type_(custom_fetch_type), |
| registry_(registry), |
| client_(client) { |
| CHECK(modulator); |
| CHECK(registry); |
| CHECK(client); |
| } |
| |
| void ModuleTreeLinker::Trace(blink::Visitor* visitor) { |
| visitor->Trace(fetch_client_settings_object_fetcher_); |
| visitor->Trace(modulator_); |
| visitor->Trace(registry_); |
| visitor->Trace(client_); |
| visitor->Trace(result_); |
| SingleModuleClient::Trace(visitor); |
| } |
| |
| #if DCHECK_IS_ON() |
| const char* ModuleTreeLinker::StateToString(ModuleTreeLinker::State state) { |
| switch (state) { |
| case State::kInitial: |
| return "Initial"; |
| case State::kFetchingSelf: |
| return "FetchingSelf"; |
| case State::kFetchingDependencies: |
| return "FetchingDependencies"; |
| case State::kInstantiating: |
| return "Instantiating"; |
| case State::kFinished: |
| return "Finished"; |
| } |
| NOTREACHED(); |
| return ""; |
| } |
| #endif |
| |
| void ModuleTreeLinker::AdvanceState(State new_state) { |
| #if DCHECK_IS_ON() |
| RESOURCE_LOADING_DVLOG(1) |
| << *this << "::advanceState(" << StateToString(state_) << " -> " |
| << StateToString(new_state) << ")"; |
| #endif |
| |
| switch (state_) { |
| case State::kInitial: |
| CHECK_EQ(num_incomplete_fetches_, 0u); |
| CHECK_EQ(new_state, State::kFetchingSelf); |
| break; |
| case State::kFetchingSelf: |
| CHECK_EQ(num_incomplete_fetches_, 0u); |
| CHECK(new_state == State::kFetchingDependencies || |
| new_state == State::kFinished); |
| break; |
| case State::kFetchingDependencies: |
| CHECK(new_state == State::kInstantiating || |
| new_state == State::kFinished); |
| break; |
| case State::kInstantiating: |
| CHECK_EQ(new_state, State::kFinished); |
| break; |
| case State::kFinished: |
| NOTREACHED(); |
| break; |
| } |
| |
| state_ = new_state; |
| |
| if (state_ == State::kFinished) { |
| #if DCHECK_IS_ON() |
| if (result_) { |
| RESOURCE_LOADING_DVLOG(1) |
| << *this << " finished with final result " << *result_; |
| } else { |
| RESOURCE_LOADING_DVLOG(1) << *this << " finished with nullptr."; |
| } |
| #endif |
| |
| registry_->ReleaseFinishedFetcher(this); |
| |
| // [IMSGF] Step 6. When the appropriate algorithm asynchronously completes |
| // with final result, asynchronously complete this algorithm with final |
| // result. |
| client_->NotifyModuleTreeLoadFinished(result_); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-module-script-tree |
| void ModuleTreeLinker::FetchRoot(const KURL& original_url, |
| const ScriptFetchOptions& options) { |
| #if DCHECK_IS_ON() |
| original_url_ = original_url; |
| root_is_inline_ = false; |
| #endif |
| |
| AdvanceState(State::kFetchingSelf); |
| |
| KURL url = original_url; |
| // <spec |
| // href="https://github.com/drufball/layered-apis/blob/master/spec.md#fetch-a-module-script-graph" |
| // step="1">Set url to the layered API fetching URL given url and the current |
| // settings object's API base URL.</spec> |
| if (RuntimeEnabledFeatures::LayeredAPIEnabled()) |
| url = blink::layered_api::ResolveFetchingURL(url); |
| |
| #if DCHECK_IS_ON() |
| url_ = url; |
| #endif |
| |
| // <spec |
| // href="https://github.com/drufball/layered-apis/blob/master/spec.md#fetch-a-module-script-graph" |
| // step="2">If url is failure, asynchronously complete this algorithm with |
| // null.</spec> |
| if (!url.IsValid()) { |
| result_ = nullptr; |
| modulator_->TaskRunner()->PostTask( |
| FROM_HERE, WTF::Bind(&ModuleTreeLinker::AdvanceState, |
| WrapPersistent(this), State::kFinished)); |
| return; |
| } |
| |
| // Step 1. Let visited set be << url >>. |
| visited_set_.insert(url); |
| |
| // Step 2. Perform the internal module script graph fetching procedure given |
| // url, settings object, destination, options, settings object, visited set, |
| // "client", and with the top-level module fetch flag set. |
| ModuleScriptFetchRequest request(url, destination_, options, |
| Referrer::ClientReferrerString(), |
| TextPosition::MinimumPosition()); |
| |
| InitiateInternalModuleScriptGraphFetching( |
| request, ModuleGraphLevel::kTopLevelModuleFetch); |
| } |
| |
| void ModuleTreeLinker::FetchRootInline(ModuleScript* module_script) { |
| // Top-level entry point for [FDaI] for an inline module script. |
| DCHECK(module_script); |
| #if DCHECK_IS_ON() |
| original_url_ = module_script->BaseURL(); |
| url_ = original_url_; |
| root_is_inline_ = true; |
| #endif |
| |
| AdvanceState(State::kFetchingSelf); |
| |
| // Store the |module_script| here which will be used as result of the |
| // algorithm when success. Also, this ensures that the |module_script| is |
| // traced via ModuleTreeLinker. |
| result_ = module_script; |
| AdvanceState(State::kFetchingDependencies); |
| |
| modulator_->TaskRunner()->PostTask( |
| FROM_HERE, |
| WTF::Bind(&ModuleTreeLinker::FetchDescendants, WrapPersistent(this), |
| WrapPersistent(module_script))); |
| } |
| |
| void ModuleTreeLinker::InitiateInternalModuleScriptGraphFetching( |
| const ModuleScriptFetchRequest& request, |
| ModuleGraphLevel level) { |
| // [IMSGF] Step 1. Assert: visited set contains url. |
| DCHECK(visited_set_.Contains(request.Url())); |
| |
| ++num_incomplete_fetches_; |
| |
| // [IMSGF] Step 2. Fetch a single module script given ... |
| modulator_->FetchSingle(request, fetch_client_settings_object_fetcher_.Get(), |
| level, custom_fetch_type_, this); |
| |
| // [IMSGF] Step 3-- are executed when NotifyModuleLoadFinished() is called. |
| } |
| |
| void ModuleTreeLinker::NotifyModuleLoadFinished(ModuleScript* module_script) { |
| // [IMSGF] Step 3. Return from this algorithm, and run the following steps |
| // when fetching a single module script asynchronously completes with result: |
| |
| CHECK_GT(num_incomplete_fetches_, 0u); |
| --num_incomplete_fetches_; |
| |
| #if DCHECK_IS_ON() |
| if (module_script) { |
| RESOURCE_LOADING_DVLOG(1) |
| << *this << "::NotifyModuleLoadFinished() with " << *module_script; |
| } else { |
| RESOURCE_LOADING_DVLOG(1) |
| << *this << "::NotifyModuleLoadFinished() with nullptr."; |
| } |
| #endif |
| |
| if (state_ == State::kFetchingSelf) { |
| // Corresponds to top-level calls to |
| // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-the-descendants-of-and-instantiate-a-module-script |
| // i.e. [IMSGF] with the top-level module fetch flag set (external), or |
| // Step 22 of "prepare a script" (inline). |
| // |module_script| is the top-level module, and will be instantiated |
| // and returned later. |
| result_ = module_script; |
| AdvanceState(State::kFetchingDependencies); |
| } |
| |
| if (state_ != State::kFetchingDependencies) { |
| // We may reach here if one of the descendant failed to load, and the other |
| // descendants fetches were in flight. |
| return; |
| } |
| |
| // Note: top-level module fetch flag is implemented so that Instantiate() |
| // is called once after all descendants are fetched, which corresponds to |
| // the single invocation of "fetch the descendants of and instantiate". |
| |
| // [IMSGF] Step 4. If result is null, asynchronously complete this algorithm |
| // with null, and abort these steps. |
| if (!module_script) { |
| result_ = nullptr; |
| AdvanceState(State::kFinished); |
| return; |
| } |
| |
| // [IMSGF] Step 5. If the top-level module fetch flag is set, fetch the |
| // descendants of and instantiate result given destination and visited set. |
| // Otherwise, fetch the descendants of result given the same arguments. |
| FetchDescendants(module_script); |
| } |
| |
| void ModuleTreeLinker::FetchDescendants(ModuleScript* module_script) { |
| DCHECK(module_script); |
| |
| // [nospec] Abort the steps if the browsing context is discarded. |
| if (!modulator_->HasValidContext()) { |
| result_ = nullptr; |
| AdvanceState(State::kFinished); |
| return; |
| } |
| |
| // [FD] Step 2. Let record be module script's record. |
| ScriptModule record = module_script->Record(); |
| |
| // [FD] Step 1. If module script's record is null, then asynchronously |
| // complete this algorithm with module script and abort these steps. |
| if (record.IsNull()) { |
| found_parse_error_ = true; |
| // We don't early-exit here and wait until all module scripts to be |
| // loaded, because we might be not sure which error to be reported. |
| // |
| // It is possible to determine whether the error to be reported can be |
| // determined without waiting for loading module scripts, and thus to |
| // early-exit here if possible. However, the complexity of such early-exit |
| // implementation might be high, and optimizing error cases with the |
| // implementation cost might be not worth doing. |
| FinalizeFetchDescendantsForOneModuleScript(); |
| return; |
| } |
| |
| // [FD] Step 3. If record.[[RequestedModules]] is empty, asynchronously |
| // complete this algorithm with module script. |
| // |
| // Note: We defer this bail-out until the end of the procedure. The rest of |
| // the procedure will be no-op anyway if record.[[RequestedModules]] is empty. |
| |
| // [FD] Step 4. Let urls be a new empty list. |
| Vector<KURL> urls; |
| Vector<TextPosition> positions; |
| |
| // [FD] Step 5. For each string requested of record.[[RequestedModules]], |
| Vector<Modulator::ModuleRequest> module_requests = |
| modulator_->ModuleRequestsFromScriptModule(record); |
| for (const auto& module_request : module_requests) { |
| // [FD] Step 5.1. Let url be the result of resolving a module specifier |
| // given module script and requested. |
| KURL url = module_script->ResolveModuleSpecifier(module_request.specifier); |
| |
| // [FD] Step 5.2. Assert: url is never failure, because resolving a module |
| // specifier must have been previously successful with these same two |
| // arguments. |
| CHECK(url.IsValid()) << "ModuleScript::ResolveModuleSpecifier() impl must " |
| "return either a valid url or null."; |
| |
| // [FD] Step 5.3. If visited set does not contain url, then: |
| if (!visited_set_.Contains(url)) { |
| // [FD] Step 5.3.1. Append url to urls. |
| urls.push_back(url); |
| |
| // [FD] Step 5.3.2. Append url to visited set. |
| visited_set_.insert(url); |
| |
| positions.push_back(module_request.position); |
| } |
| } |
| |
| if (urls.IsEmpty()) { |
| // [FD] Step 3. If record.[[RequestedModules]] is empty, asynchronously |
| // complete this algorithm with module script. |
| // |
| // Also, if record.[[RequestedModules]] is not empty but |urls| is |
| // empty here, we complete this algorithm. |
| FinalizeFetchDescendantsForOneModuleScript(); |
| return; |
| } |
| |
| // [FD] Step 6. Let options be the descendant script fetch options for module |
| // script's fetch options. |
| // https://html.spec.whatwg.org/multipage/webappapis.html#descendant-script-fetch-options |
| // the descendant script fetch options are a new script fetch options whose |
| // items all have the same values, except for the integrity metadata, which is |
| // instead the empty string. |
| ScriptFetchOptions options(module_script->FetchOptions().Nonce(), |
| IntegrityMetadataSet(), String(), |
| module_script->FetchOptions().ParserState(), |
| module_script->FetchOptions().CredentialsMode(), |
| module_script->FetchOptions().GetReferrerPolicy()); |
| |
| // [FD] Step 7. For each url in urls, ... |
| // |
| // [FD] Step 7. These invocations of the internal module script graph fetching |
| // procedure should be performed in parallel to each other. |
| for (wtf_size_t i = 0; i < urls.size(); ++i) { |
| // [FD] Step 7. ... perform the internal module script graph fetching |
| // procedure given url, fetch client settings object, destination, options, |
| // module script's settings object, visited set, module script's base URL, |
| // and with the top-level module fetch flag unset. ... |
| ModuleScriptFetchRequest request(urls[i], destination_, options, |
| module_script->BaseURL().GetString(), |
| positions[i]); |
| InitiateInternalModuleScriptGraphFetching( |
| request, ModuleGraphLevel::kDependentModuleFetch); |
| } |
| |
| // Asynchronously continue processing after NotifyModuleLoadFinished() is |
| // called num_incomplete_fetches_ times. |
| CHECK_GT(num_incomplete_fetches_, 0u); |
| } |
| |
| void ModuleTreeLinker::FinalizeFetchDescendantsForOneModuleScript() { |
| // [FD] of a single module script is completed here: |
| // |
| // [FD] Step 7. Otherwise, wait until all of the internal module script graph |
| // fetching procedure invocations have asynchronously completed. ... |
| |
| // And, if |num_incomplete_fetches_| is 0, all the invocations of [FD] |
| // (called from [FDaI] Step 2) of the root module script is completed here |
| // and thus we proceed to [FDaI] Step 4 implemented by Instantiate(). |
| if (num_incomplete_fetches_ == 0) |
| Instantiate(); |
| } |
| |
| void ModuleTreeLinker::Instantiate() { |
| // [nospec] Abort the steps if the browsing context is discarded. |
| if (!modulator_->HasValidContext()) { |
| result_ = nullptr; |
| AdvanceState(State::kFinished); |
| return; |
| } |
| |
| // [FDaI] Step 4. If result is null, then asynchronously complete this |
| // algorithm with result. |
| if (!result_) { |
| AdvanceState(State::kFinished); |
| return; |
| } |
| |
| // [FDaI] Step 6. If parse error is null, then: |
| // |
| // [Optimization] If |found_parse_error_| is false (i.e. no parse errors |
| // were found during fetching), we are sure that |parse error| is null and |
| // thus skip FindFirstParseError() call. |
| if (!found_parse_error_) { |
| #if DCHECK_IS_ON() |
| HeapHashSet<Member<ModuleScript>> discovered_set; |
| DCHECK(FindFirstParseError(result_, &discovered_set).IsEmpty()); |
| #endif |
| |
| // [FDaI] Step 6.1. Let record be result's record. |
| ScriptModule record = result_->Record(); |
| |
| // [FDaI] Step 6.2. Perform record.Instantiate(). |
| AdvanceState(State::kInstantiating); |
| ScriptValue instantiation_error = modulator_->InstantiateModule(record); |
| |
| // [FDaI] Step 6.2. If this throws an exception, set result's error to |
| // rethrow to that exception. |
| if (!instantiation_error.IsEmpty()) |
| result_->SetErrorToRethrow(instantiation_error); |
| } else { |
| // [FDaI] Step 7. Otherwise ... |
| |
| // [FFPE] Step 2. If discoveredSet was not given, let it be an empty set. |
| HeapHashSet<Member<ModuleScript>> discovered_set; |
| |
| // [FDaI] Step 5. Let parse error be the result of finding the first parse |
| // error given result. |
| ScriptValue parse_error = FindFirstParseError(result_, &discovered_set); |
| DCHECK(!parse_error.IsEmpty()); |
| |
| // [FDaI] Step 7. ... set result's error to rethrow to parse error. |
| result_->SetErrorToRethrow(parse_error); |
| } |
| |
| // [FDaI] Step 8. Asynchronously complete this algorithm with result. |
| AdvanceState(State::kFinished); |
| } |
| |
| // [FFPE] https://html.spec.whatwg.org/#finding-the-first-parse-error |
| // |
| // This returns non-empty ScriptValue iff a parse error is found. |
| ScriptValue ModuleTreeLinker::FindFirstParseError( |
| ModuleScript* module_script, |
| HeapHashSet<Member<ModuleScript>>* discovered_set) const { |
| // FindFirstParseError() is called only when there is no fetch errors, i.e. |
| // all module scripts in the graph are non-null. |
| DCHECK(module_script); |
| |
| // [FFPE] Step 1. Let moduleMap be moduleScript's settings object's module |
| // map. |
| // |
| // This is accessed via |modulator_|. |
| |
| // [FFPE] Step 2 is done before calling this in Instantiate(). |
| |
| // [FFPE] Step 3. Append moduleScript to discoveredSet. |
| discovered_set->insert(module_script); |
| |
| // [FFPE] Step 4. If moduleScript's record is null, then return moduleScript's |
| // parse error. |
| ScriptModule record = module_script->Record(); |
| if (record.IsNull()) |
| return module_script->CreateParseError(); |
| |
| // [FFPE] Step 5. Let childSpecifiers be the value of moduleScript's record's |
| // [[RequestedModules]] internal slot. |
| Vector<Modulator::ModuleRequest> child_specifiers = |
| modulator_->ModuleRequestsFromScriptModule(record); |
| |
| for (const auto& module_request : child_specifiers) { |
| // [FFPE] Step 6. Let childURLs be the list obtained by calling resolve a |
| // module specifier once for each item of childSpecifiers, given |
| // moduleScript and that item. |
| KURL child_url = |
| module_script->ResolveModuleSpecifier(module_request.specifier); |
| |
| // [FFPE] Step 6. ... (None of these will ever fail, as otherwise |
| // moduleScript would have been marked as itself having a parse error.) |
| CHECK(child_url.IsValid()) |
| << "ModuleScript::ResolveModuleSpecifier() impl must " |
| "return either a valid url or null."; |
| |
| // [FFPE] Step 7. Let childModules be the list obtained by getting each |
| // value in moduleMap whose key is given by an item of childURLs. |
| // |
| // [FFPE] Step 8. For each childModule of childModules: |
| ModuleScript* child_module = modulator_->GetFetchedModuleScript(child_url); |
| |
| // [FFPE] Step 8.1. Assert: childModule is a module script (i.e., it is not |
| // "fetching" or null) |
| CHECK(child_module); |
| |
| // [FFPE] Step 8.2. If discoveredSet already contains childModule, continue. |
| if (discovered_set->Contains(child_module)) |
| continue; |
| |
| // [FFPE] Step 8.3. Let childParseError be the result of finding the first |
| // parse error given childModule and discoveredSet. |
| ScriptValue child_parse_error = |
| FindFirstParseError(child_module, discovered_set); |
| |
| // [FFPE] Step 8.4. If childParseError is not null, return childParseError. |
| if (!child_parse_error.IsEmpty()) |
| return child_parse_error; |
| } |
| |
| // [FFPE] Step 9. Return null. |
| return ScriptValue(); |
| } |
| |
| #if DCHECK_IS_ON() |
| std::ostream& operator<<(std::ostream& stream, const ModuleTreeLinker& linker) { |
| stream << "ModuleTreeLinker[" << &linker |
| << ", original_url=" << linker.original_url_.GetString() |
| << ", url=" << linker.url_.GetString() |
| << ", inline=" << linker.root_is_inline_ << "]"; |
| return stream; |
| } |
| #endif |
| |
| } // namespace blink |