Job.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. * Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibJS/Heap/Heap.h>
  7. #include <LibJS/Runtime/VM.h>
  8. #include <LibURL/URL.h>
  9. #include <LibWeb/HTML/Scripting/Environments.h>
  10. #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
  11. #include <LibWeb/SecureContexts/AbstractOperations.h>
  12. #include <LibWeb/ServiceWorker/Job.h>
  13. #include <LibWeb/ServiceWorker/Registration.h>
  14. #include <LibWeb/WebIDL/Promise.h>
  15. namespace Web::ServiceWorker {
  16. static void run_job(JS::VM&, JobQueue&);
  17. static void finish_job(JS::VM&, JS::NonnullGCPtr<Job>);
  18. static void resolve_job_promise(JS::NonnullGCPtr<Job>, Optional<Registration const&>, JS::Value = JS::js_null());
  19. template<typename Error>
  20. static void reject_job_promise(JS::NonnullGCPtr<Job>, FlyString message);
  21. static void register_(JS::VM&, JS::NonnullGCPtr<Job>);
  22. static void update(JS::VM&, JS::NonnullGCPtr<Job>);
  23. static void unregister(JS::VM&, JS::NonnullGCPtr<Job>);
  24. JS_DEFINE_ALLOCATOR(Job);
  25. // https://w3c.github.io/ServiceWorker/#create-job-algorithm
  26. JS::NonnullGCPtr<Job> Job::create(JS::VM& vm, Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, JS::GCPtr<WebIDL::Promise> promise, JS::GCPtr<HTML::EnvironmentSettingsObject> client)
  27. {
  28. return vm.heap().allocate_without_realm<Job>(type, move(storage_key), move(scope_url), move(script_url), promise, client);
  29. }
  30. Job::Job(Job::Type type, StorageAPI::StorageKey storage_key, URL::URL scope_url, URL::URL script_url, JS::GCPtr<WebIDL::Promise> promise, JS::GCPtr<HTML::EnvironmentSettingsObject> client)
  31. : job_type(type)
  32. , storage_key(move(storage_key))
  33. , scope_url(move(scope_url))
  34. , script_url(move(script_url))
  35. , client(client)
  36. , job_promise(promise)
  37. {
  38. // 8. If client is not null, set job’s referrer to client’s creation URL.
  39. if (client)
  40. referrer = client->creation_url;
  41. }
  42. Job::~Job() = default;
  43. void Job::visit_edges(JS::Cell::Visitor& visitor)
  44. {
  45. Base::visit_edges(visitor);
  46. visitor.visit(client);
  47. visitor.visit(job_promise);
  48. for (auto& job : list_of_equivalent_jobs)
  49. visitor.visit(job);
  50. }
  51. // FIXME: Does this need to be a 'user agent' level thing? Or can we have one per renderer process?
  52. // https://w3c.github.io/ServiceWorker/#dfn-scope-to-job-queue-map
  53. static HashMap<ByteString, JobQueue>& scope_to_job_queue_map()
  54. {
  55. static HashMap<ByteString, JobQueue> map;
  56. return map;
  57. }
  58. // https://w3c.github.io/ServiceWorker/#register-algorithm
  59. static void register_(JS::VM& vm, JS::NonnullGCPtr<Job> job)
  60. {
  61. auto script_origin = job->script_url.origin();
  62. auto scope_origin = job->scope_url.origin();
  63. auto referrer_origin = job->referrer->origin();
  64. // 1. If the result of running potentially trustworthy origin with the origin of job’s script url as the argument is Not Trusted, then:
  65. if (SecureContexts::Trustworthiness::NotTrustworthy == SecureContexts::is_origin_potentially_trustworthy(script_origin)) {
  66. // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
  67. reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has untrustworthy script origin"_fly_string);
  68. // 2. Invoke Finish Job with job and abort these steps.
  69. finish_job(vm, job);
  70. return;
  71. }
  72. // 2. If job’s script url's origin and job’s referrer's origin are not same origin, then:
  73. if (!script_origin.is_same_origin(referrer_origin)) {
  74. // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
  75. reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has incompatible script and referrer origins"_fly_string);
  76. // 2. Invoke Finish Job with job and abort these steps.
  77. finish_job(vm, job);
  78. return;
  79. }
  80. // 3. If job’s scope url's origin and job’s referrer's origin are not same origin, then:
  81. if (!scope_origin.is_same_origin(referrer_origin)) {
  82. // 1. Invoke Reject Job Promise with job and "SecurityError" DOMException.
  83. reject_job_promise<WebIDL::SecurityError>(job, "Service Worker registration has incompatible scope and referrer origins"_fly_string);
  84. // 2. Invoke Finish Job with job and abort these steps.
  85. finish_job(vm, job);
  86. return;
  87. }
  88. // 4. Let registration be the result of running Get Registration given job’s storage key and job’s scope url.
  89. auto registration = Registration::get(job->storage_key, job->scope_url);
  90. // 5. If registration is not null, then:
  91. if (registration.has_value()) {
  92. // 1. Let newestWorker be the result of running the Get Newest Worker algorithm passing registration as the argument.
  93. auto* newest_worker = registration->newest_worker();
  94. // 2. If newestWorker is not null, job’s script url equals newestWorker’s script url,
  95. // job’s worker type equals newestWorker’s type, and job’s update via cache mode's value equals registration’s update via cache mode, then:
  96. if (newest_worker != nullptr
  97. && job->script_url == newest_worker->script_url
  98. && job->worker_type == newest_worker->worker_type
  99. && job->update_via_cache == registration->update_via_cache()) {
  100. // 1. Invoke Resolve Job Promise with job and registration.
  101. resolve_job_promise(job, registration.value());
  102. // 2. Invoke Finish Job with job and abort these steps.
  103. finish_job(vm, job);
  104. return;
  105. }
  106. }
  107. // 6. Else:
  108. else {
  109. // 1. Invoke Set Registration algorithm with job’s storage key, job’s scope url, and job’s update via cache mode.
  110. Registration::set(job->storage_key, job->scope_url, job->update_via_cache);
  111. }
  112. // Invoke Update algorithm passing job as the argument.
  113. update(vm, job);
  114. }
  115. static void update(JS::VM& vm, JS::NonnullGCPtr<Job> job)
  116. {
  117. // If there's no client, there won't be any promises to resolve
  118. if (job->client) {
  119. auto context = HTML::TemporaryExecutionContext(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  120. auto& realm = *vm.current_realm();
  121. WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Service Worker update"sv).value());
  122. }
  123. }
  124. static void unregister(JS::VM& vm, JS::NonnullGCPtr<Job> job)
  125. {
  126. // If there's no client, there won't be any promises to resolve
  127. if (job->client) {
  128. auto context = HTML::TemporaryExecutionContext(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  129. auto& realm = *vm.current_realm();
  130. WebIDL::reject_promise(realm, *job->job_promise, *vm.throw_completion<JS::InternalError>(JS::ErrorType::NotImplemented, "Service Worker unregistration"sv).value());
  131. }
  132. }
  133. // https://w3c.github.io/ServiceWorker/#run-job-algorithm
  134. static void run_job(JS::VM& vm, JobQueue& job_queue)
  135. {
  136. // 1. Assert: jobQueue is not empty.
  137. VERIFY(!job_queue.is_empty());
  138. // 2. Queue a task to run these steps:
  139. auto job_run_steps = JS::create_heap_function(vm.heap(), [&vm, &job_queue] {
  140. // 1. Let job be the first item in jobQueue.
  141. auto& job = job_queue.first();
  142. // FIXME: Do these really need to be in parallel to the HTML event loop? Sounds fishy
  143. switch (job->job_type) {
  144. case Job::Type::Register:
  145. // 2. If job’s job type is register, run Register with job in parallel.
  146. register_(vm, job);
  147. break;
  148. case Job::Type::Update:
  149. // 3. If job’s job type is update, run Update with job in parallel.
  150. update(vm, job);
  151. break;
  152. case Job::Type::Unregister:
  153. // 4. If job’s job type is unregister, run Unregister with job in parallel.
  154. unregister(vm, job);
  155. break;
  156. }
  157. });
  158. // FIXME: How does the user agent ensure this happens? Is this a normative note?
  159. // Spec-Note:
  160. // For a register job and an update job, the user agent delays queuing a task for running the job
  161. // until after a DOMContentLoaded event has been dispatched to the document that initiated the job.
  162. // FIXME: Spec should be updated to avoid 'queue a task' and use 'queue a global task' instead
  163. // FIXME: On which task source? On which event loop? On behalf of which document?
  164. HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, job_run_steps);
  165. }
  166. // https://w3c.github.io/ServiceWorker/#finish-job-algorithm
  167. static void finish_job(JS::VM& vm, JS::NonnullGCPtr<Job> job)
  168. {
  169. // 1. Let jobQueue be job’s containing job queue.
  170. auto& job_queue = *job->containing_job_queue;
  171. // 2. Assert: the first item in jobQueue is job.
  172. VERIFY(job_queue.first() == job);
  173. // 3. Dequeue from jobQueue
  174. (void)job_queue.take_first();
  175. // 4. If jobQueue is not empty, invoke Run Job with jobQueue.
  176. if (!job_queue.is_empty())
  177. run_job(vm, job_queue);
  178. }
  179. // https://w3c.github.io/ServiceWorker/#resolve-job-promise-algorithm
  180. static void resolve_job_promise(JS::NonnullGCPtr<Job> job, Optional<Registration const&>, JS::Value value)
  181. {
  182. // 1. If job’s client is not null, queue a task, on job’s client's responsible event loop using the DOM manipulation task source, to run the following substeps:
  183. if (job->client) {
  184. auto& realm = job->client->realm();
  185. HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, value] {
  186. HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  187. // FIXME: Resolve to a ServiceWorkerRegistration platform object
  188. // 1. Let convertedValue be null.
  189. // 2. If job’s job type is either register or update, set convertedValue to the result of
  190. // getting the service worker registration object that represents value in job’s client.
  191. // 3. Else, set convertedValue to value, in job’s client's Realm.
  192. // 4. Resolve job’s job promise with convertedValue.
  193. WebIDL::resolve_promise(realm, *job->job_promise, value);
  194. }));
  195. }
  196. // 2. For each equivalentJob in job’s list of equivalent jobs:
  197. for (auto& equivalent_job : job->list_of_equivalent_jobs) {
  198. // 1. If equivalentJob’s client is null, continue.
  199. if (!equivalent_job->client)
  200. continue;
  201. // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source,
  202. // to run the following substeps:
  203. auto& realm = equivalent_job->client->realm();
  204. HTML::queue_a_task(HTML::Task::Source::DOMManipulation, equivalent_job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, equivalent_job, value] {
  205. HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  206. // FIXME: Resolve to a ServiceWorkerRegistration platform object
  207. // 1. Let convertedValue be null.
  208. // 2. If equivalentJob’s job type is either register or update, set convertedValue to the result of
  209. // getting the service worker registration object that represents value in equivalentJob’s client.
  210. // 3. Else, set convertedValue to value, in equivalentJob’s client's Realm.
  211. // 4. Resolve equivalentJob’s job promise with convertedValue.
  212. WebIDL::resolve_promise(realm, *equivalent_job->job_promise, value);
  213. }));
  214. }
  215. }
  216. // https://w3c.github.io/ServiceWorker/#reject-job-promise-algorithm
  217. template<typename Error>
  218. static void reject_job_promise(JS::NonnullGCPtr<Job> job, FlyString message)
  219. {
  220. // 1. If job’s client is not null, queue a task, on job’s client's responsible event loop using the DOM manipulation task source,
  221. // to reject job’s job promise with a new exception with errorData and a user agent-defined message, in job’s client's Realm.
  222. if (job->client) {
  223. auto& realm = job->client->realm();
  224. HTML::queue_a_task(HTML::Task::Source::DOMManipulation, job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, job, message] {
  225. HTML::TemporaryExecutionContext const context(*job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  226. WebIDL::reject_promise(realm, *job->job_promise, Error::create(realm, message));
  227. }));
  228. }
  229. // 2. For each equivalentJob in job’s list of equivalent jobs:
  230. for (auto& equivalent_job : job->list_of_equivalent_jobs) {
  231. // 1. If equivalentJob’s client is null, continue.
  232. if (!equivalent_job->client)
  233. continue;
  234. // 2. Queue a task, on equivalentJob’s client's responsible event loop using the DOM manipulation task source,
  235. // to reject equivalentJob’s job promise with a new exception with errorData and a user agent-defined message,
  236. // in equivalentJob’s client's Realm.
  237. auto& realm = equivalent_job->client->realm();
  238. HTML::queue_a_task(HTML::Task::Source::DOMManipulation, equivalent_job->client->responsible_event_loop(), nullptr, JS::create_heap_function(realm.heap(), [&realm, equivalent_job, message] {
  239. HTML::TemporaryExecutionContext const context(*equivalent_job->client, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
  240. WebIDL::reject_promise(realm, *equivalent_job->job_promise, Error::create(realm, message));
  241. }));
  242. }
  243. }
  244. // https://w3c.github.io/ServiceWorker/#schedule-job-algorithm
  245. void schedule_job(JS::VM& vm, JS::NonnullGCPtr<Job> job)
  246. {
  247. // 1. Let jobQueue be null.
  248. // Note: See below for how we ensure job queue
  249. // 2. Let jobScope be job’s scope url, serialized.
  250. // FIXME: Suspect that spec should specify to not use fragment here
  251. auto job_scope = job->scope_url.serialize();
  252. // 3. If scope to job queue map[jobScope] does not exist, set scope to job queue map[jobScope] to a new job queue.
  253. // 4. Set jobQueue to scope to job queue map[jobScope].
  254. auto& job_queue = scope_to_job_queue_map().ensure(job_scope, [&vm] {
  255. return JobQueue(vm.heap());
  256. });
  257. // 5. If jobQueue is empty, then:
  258. if (job_queue.is_empty()) {
  259. // 2. Set job’s containing job queue to jobQueue, and enqueue job to jobQueue.
  260. job->containing_job_queue = &job_queue;
  261. job_queue.append(job);
  262. run_job(vm, job_queue);
  263. }
  264. // 6. Else:
  265. else {
  266. // 1. Let lastJob be the element at the back of jobQueue.
  267. auto& last_job = job_queue.last();
  268. // 2. If job is equivalent to lastJob and lastJob’s job promise has not settled, append job to lastJob’s list of equivalent jobs.
  269. // FIXME: There's no WebIDL AO that corresponds to checking if an ECMAScript promise has settled
  270. if (job == last_job && !verify_cast<JS::Promise>(*job->job_promise->promise()).is_handled()) {
  271. last_job->list_of_equivalent_jobs.append(job);
  272. }
  273. // 3. Else, set job’s containing job queue to jobQueue, and enqueue job to jobQueue.
  274. else {
  275. job->containing_job_queue = &job_queue;
  276. job_queue.append(job);
  277. }
  278. }
  279. }
  280. }