| // Copyright 2022 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/worker/worker_global_scope.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "base/message_loop/message_loop_current.h" |
| #include "base/threading/thread.h" |
| #include "base/trace_event/trace_event.h" |
| #include "cobalt/loader/origin.h" |
| #include "cobalt/script/environment_settings.h" |
| #include "cobalt/web/context.h" |
| #include "cobalt/web/dom_exception.h" |
| #include "cobalt/web/user_agent_platform_info.h" |
| #include "cobalt/web/window_or_worker_global_scope.h" |
| #include "cobalt/web/window_timers.h" |
| #include "cobalt/worker/service_worker_object.h" |
| #include "cobalt/worker/worker_location.h" |
| #include "cobalt/worker/worker_navigator.h" |
| #include "starboard/atomic.h" |
| #include "url/gurl.h" |
| |
| namespace cobalt { |
| namespace worker { |
| |
| namespace { |
| bool PermitAnyURL(const GURL& url, bool) { return true; } |
| |
| class ScriptLoader : public base::MessageLoop::DestructionObserver { |
| public: |
| explicit ScriptLoader(web::Context* context) : context_(context) { |
| thread_.reset(new base::Thread("ImportScriptsLoader")); |
| thread_->Start(); |
| |
| // Register as a destruction observer to shut down the Loaders once all |
| // pending tasks have been executed and the message loop is about to be |
| // destroyed. This allows us to safely stop the thread, drain the task |
| // queue, then destroy the internal components before the message loop is |
| // reset. No posted tasks will be executed once the thread is stopped. |
| thread_->message_loop()->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&base::MessageLoop::AddDestructionObserver, |
| base::Unretained(thread_->message_loop()), |
| base::Unretained(this))); |
| |
| thread_->message_loop()->task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](loader::FetcherFactory* fetcher_factory, |
| std::unique_ptr<loader::ScriptLoaderFactory>* |
| script_loader_factory_) { |
| TRACE_EVENT0("cobalt::worker", |
| "ScriptLoader::ScriptLoaderFactory Task"); |
| script_loader_factory_->reset(new loader::ScriptLoaderFactory( |
| "ImportScriptsLoader", fetcher_factory)); |
| }, |
| context_->fetcher_factory(), &script_loader_factory_)); |
| } |
| |
| |
| ~ScriptLoader() { |
| if (thread_) { |
| // Stop the thread. This will cause the destruction observer to be |
| // notified. |
| thread_->Stop(); |
| } |
| } |
| |
| void WillDestroyCurrentMessageLoop() { |
| // Destroy members that were constructed in the worker thread. |
| errors_.clear(); |
| errors_.shrink_to_fit(); |
| contents_.clear(); |
| contents_.shrink_to_fit(); |
| loaders_.clear(); |
| loaders_.shrink_to_fit(); |
| script_loader_factory_.reset(); |
| } |
| |
| void Load(const loader::Origin& origin, |
| const std::vector<GURL>& resolved_urls) { |
| TRACE_EVENT0("cobalt::worker", "ScriptLoader::Load()"); |
| number_of_loads_ = resolved_urls.size(); |
| contents_.resize(resolved_urls.size()); |
| loaders_.resize(resolved_urls.size()); |
| errors_.resize(resolved_urls.size()); |
| if (resolved_urls.empty()) return; |
| |
| for (int i = 0; i < resolved_urls.size(); ++i) { |
| const GURL& url = resolved_urls[i]; |
| thread_->message_loop()->task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&ScriptLoader::LoaderTask, |
| base::Unretained(this), &loaders_[i], |
| origin, url, &contents_[i], &errors_[i])); |
| } |
| load_finished_.Wait(); |
| } |
| |
| void LoaderTask(std::unique_ptr<loader::Loader>* loader, |
| const loader::Origin& origin, const GURL& url, |
| std::unique_ptr<std::string>* content, |
| std::unique_ptr<std::string>* error) { |
| TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoaderTask()"); |
| // Todo: implement csp check (b/225037465) |
| csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL); |
| |
| // If there is a request callback, call it to possibly retrieve previously |
| // requested content. |
| *loader = script_loader_factory_->CreateScriptLoader( |
| url, origin, csp_callback, |
| base::Bind( |
| [](std::unique_ptr<std::string>* output_content, |
| const loader::Origin& last_url_origin, |
| std::unique_ptr<std::string> content) { |
| *output_content = std::move(content); |
| }, |
| content), |
| base::Bind(&ScriptLoader::LoadingCompleteCallback, |
| base::Unretained(this), loader, error)); |
| } |
| |
| void LoadingCompleteCallback(std::unique_ptr<loader::Loader>* loader, |
| std::unique_ptr<std::string>* output_error, |
| const base::Optional<std::string>& error) { |
| TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoadingCompleteCallback()"); |
| if (error) { |
| output_error->reset(new std::string(std::move(error.value()))); |
| } |
| if (!SbAtomicNoBarrier_Increment(&number_of_loads_, -1)) { |
| // Clear the loader factory after this callback |
| // completes. |
| base::MessageLoop::current()->task_runner()->PostTask( |
| FROM_HERE, base::BindOnce( |
| [](base::WaitableEvent* load_finished_) { |
| load_finished_->Signal(); |
| }, |
| &load_finished_)); |
| } |
| } |
| |
| const std::unique_ptr<std::string>& GetContents(int index) { |
| return contents_[index]; |
| } |
| |
| const std::unique_ptr<std::string>& GetError(int index) { |
| return errors_[index]; |
| } |
| |
| private: |
| web::Context* context_; |
| base::WaitableEvent load_finished_ = { |
| base::WaitableEvent::ResetPolicy::MANUAL, |
| base::WaitableEvent::InitialState::NOT_SIGNALED}; |
| |
| std::unique_ptr<base::Thread> thread_; |
| |
| std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_; |
| |
| volatile SbAtomic32 number_of_loads_; |
| |
| std::vector<std::unique_ptr<std::string>> contents_; |
| std::vector<std::unique_ptr<std::string>> errors_; |
| std::vector<std::unique_ptr<loader::Loader>> loaders_; |
| |
| base::Optional<std::string> error_; |
| }; |
| |
| } // namespace |
| |
| WorkerGlobalScope::WorkerGlobalScope(script::EnvironmentSettings* settings) |
| : web::WindowOrWorkerGlobalScope( |
| settings, /*stat_tracker=*/NULL, |
| // TODO (b/233788170): once application state is |
| // available, update this to use the actual state. |
| base::ApplicationState::kApplicationStateStarted), |
| location_(new WorkerLocation(settings->creation_url())), |
| navigator_(new WorkerNavigator(settings)) { |
| set_navigator_base(navigator_); |
| } |
| |
| void WorkerGlobalScope::ImportScripts(const std::vector<std::string>& urls, |
| script::ExceptionState* exception_state) { |
| ImportScriptsInternal(urls, exception_state, URLLookupCallback(), |
| ResponseCallback()); |
| } |
| |
| bool WorkerGlobalScope::LoadImportsAndReturnIfUpdated( |
| const ScriptResourceMap& previous_resource_map, |
| ScriptResourceMap* new_resource_map) { |
| bool has_updated_resources = false; |
| // Steps from Algorithm for Update: |
| // https://w3c.github.io/ServiceWorker/#update-algorithm |
| // 8.21.1. For each importUrl -> storedResponse of newestWorker’s script |
| // resource map: |
| std::vector<GURL> request_urls; |
| for (const auto& resource_map_entry : previous_resource_map) { |
| // 8.21.1.1. If importUrl is url, then continue. |
| if (new_resource_map->find(resource_map_entry.first) != |
| new_resource_map->end()) { |
| continue; |
| } |
| request_urls.push_back(resource_map_entry.first); |
| } |
| |
| // 8.21.1.2. Let importRequest be a new request whose url is importUrl, |
| // client is job’s client, destination is "script", parser |
| // metadata is "not parser-inserted", synchronous flag is set, |
| // and whose use-URL-credentials flag is set. |
| // 8.21.1.3. Set importRequest’s cache mode to "no-cache" if any of the |
| // following are true: |
| // - registration’s update via cache mode is "none". |
| // - job’s force bypass cache flag is set. |
| // - registration is stale. |
| // 8.21.1.4. Let fetchedResponse be the result of fetching importRequest. |
| web::EnvironmentSettings* settings = environment_settings(); |
| const GURL& base_url = settings->base_url(); |
| loader::Origin origin = loader::Origin(base_url.GetOrigin()); |
| ScriptLoader script_loader(settings->context()); |
| script_loader.Load(origin, request_urls); |
| |
| for (int index = 0; index < request_urls.size(); ++index) { |
| const auto& error = script_loader.GetError(index); |
| if (error) continue; |
| const GURL& url = request_urls[index]; |
| const std::unique_ptr<std::string>& script = |
| script_loader.GetContents(index); |
| // 8.21.1.5. Set updatedResourceMap[importRequest’s url] to |
| // fetchedResponse. |
| (*new_resource_map)[url].reset(new std::string(*script.get())); |
| // 8.21.1.6. Set fetchedResponse to fetchedResponse’s unsafe response. |
| // 8.21.1.7. If fetchedResponse’s cache state is not |
| // "local", set registration’s last update check time to the |
| // current time. |
| // 8.21.1.8. If fetchedResponse is a bad import script response, continue. |
| // 8.21.1.9. If fetchedResponse’s body is not byte-for-byte identical with |
| // storedResponse’s unsafe response's body, set |
| // hasUpdatedResources to true. |
| DCHECK(previous_resource_map.find(url) != previous_resource_map.end()); |
| if (*script != *(previous_resource_map.find(url)->second)) { |
| has_updated_resources = true; |
| } |
| } |
| return has_updated_resources; |
| } |
| |
| void WorkerGlobalScope::ImportScriptsInternal( |
| const std::vector<std::string>& urls, |
| script::ExceptionState* exception_state, |
| URLLookupCallback url_lookup_callback, ResponseCallback response_callback) { |
| // Algorithm for import scripts into worker global scope: |
| // https://html.spec.whatwg.org/multipage/workers.html#import-scripts-into-worker-global-scope |
| |
| // 1. If worker global scope's type is "module", throw a TypeError exception. |
| // Cobalt does not support "module" type scripts. |
| |
| // 2. Let settings object be the current settings object. |
| web::EnvironmentSettings* settings = environment_settings(); |
| DCHECK(settings->context() |
| ->message_loop() |
| ->task_runner() |
| ->BelongsToCurrentThread()); |
| |
| // 3. If urls is empty, return. |
| if (urls.empty()) return; |
| |
| // 4. Parse each value in urls relative to settings object. If any fail, throw |
| // a "SyntaxError" DOMException. |
| std::vector<GURL> request_urls; |
| std::vector<std::string*> looked_up_content; |
| request_urls.reserve(urls.size()); |
| std::vector<GURL> resolved_urls(urls.size()); |
| std::vector<int> request_url_indexes(urls.size()); |
| |
| const GURL& base_url = settings->base_url(); |
| for (int index = 0; index < urls.size(); ++index) { |
| const std::string& url = urls[index]; |
| resolved_urls[index] = base_url.Resolve(url); |
| if (resolved_urls[index].is_empty()) { |
| web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state); |
| return; |
| } |
| std::string* content = |
| url_lookup_callback |
| ? url_lookup_callback.Run(resolved_urls[index], exception_state) |
| : nullptr; |
| // Return if the url lookup callback has set the exception state. |
| if (exception_state->is_exception_set()) return; |
| if (content) { |
| // Store the result of the url lookup callback. |
| request_url_indexes[index] = -1; |
| looked_up_content.push_back(content); |
| } else { |
| // Add the url to the list to pass to ScriptLoader for loading. |
| request_url_indexes[index] = request_urls.size(); |
| request_urls.push_back(resolved_urls[index]); |
| } |
| } |
| |
| loader::Origin origin = loader::Origin(base_url.GetOrigin()); |
| |
| // 5. For each url in the resulting URL records, run these substeps: |
| // 5.1. Fetch a classic worker-imported script given url and settings |
| // object, passing along any custom perform the fetch steps provided. |
| // If this succeeds, let script be the result. Otherwise, rethrow the |
| // exception. |
| ScriptLoader script_loader(settings->context()); |
| script_loader.Load(origin, request_urls); |
| |
| // 5. For each url in the resulting URL records, run these substeps: |
| int content_lookup_index = 0; |
| for (int index = 0; index < resolved_urls.size(); ++index) { |
| int request_index = request_url_indexes[index]; |
| std::string* script = nullptr; |
| |
| if (request_index == -1) { |
| // The content at this index was received from the url lookup callback. |
| script = looked_up_content[content_lookup_index++]; |
| } else { |
| const auto& error = script_loader.GetError(request_index); |
| if (error) { |
| LOG(WARNING) << "Script Loading Failed : " << *error; |
| web::DOMException::Raise(web::DOMException::kNetworkErr, *error, |
| exception_state); |
| break; |
| } |
| script = script_loader.GetContents(request_index).get(); |
| } |
| // 5.2. Run the classic script script, with the rethrow errors argument |
| // set to true. |
| if (script) { |
| if (response_callback) { |
| script = response_callback.Run(resolved_urls[index], script); |
| } |
| bool succeeded = false; |
| std::string retval; |
| if (script) { |
| bool mute_errors = false; |
| const base::SourceLocation script_location(resolved_urls[index].spec(), |
| 1, 1); |
| retval = settings->context()->script_runner()->Execute( |
| *script, script_location, mute_errors, &succeeded); |
| } |
| if (!succeeded) { |
| // TODO(): Handle script execution errors. |
| LOG(WARNING) << "Script Execution Failed : " << retval; |
| web::DOMException::Raise(web::DOMException::kSyntaxErr, |
| exception_state); |
| return; |
| } |
| } |
| } |
| // If an exception was thrown or if the script was prematurely aborted, then |
| // abort all these steps, letting the exception or aborting continue to be |
| // processed by the calling script. |
| } |
| |
| } // namespace worker |
| } // namespace cobalt |