Skip to main content

script/dom/workers/
serviceworkercontainer.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::VecDeque;
6use std::default::Default;
7use std::rc::Rc;
8
9use dom_struct::dom_struct;
10use js::context::JSContext;
11use js::jsval::UndefinedValue;
12use js::realm::CurrentRealm;
13use script_bindings::cell::DomRefCell;
14use script_bindings::inheritance::Castable;
15use script_bindings::reflector::reflect_dom_object_with_cx;
16use servo_base::generic_channel::GenericCallback;
17use servo_constellation_traits::{
18    Job, JobError, JobResult, JobResultValue, JobType, ScriptToConstellationMessage,
19    ServiceWorkerAlgorithm, ServiceWorkerAlgorithmResult, ServiceWorkerRegistrationInfo,
20};
21use servo_url::{ImmutableOrigin, ServoUrl};
22
23use crate::dom::bindings::codegen::Bindings::ServiceWorkerContainerBinding::{
24    RegistrationOptions, ServiceWorkerContainerMethods,
25};
26use crate::dom::bindings::error::Error;
27use crate::dom::bindings::refcounted::Trusted;
28use crate::dom::bindings::reflector::DomGlobal;
29use crate::dom::bindings::root::{DomRoot, MutNullableDom};
30use crate::dom::bindings::str::USVString;
31use crate::dom::bindings::structuredclone;
32use crate::dom::eventtarget::EventTarget;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::promise::Promise;
35use crate::dom::serviceworker::ServiceWorker;
36use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
37use crate::dom::types::MessageEvent;
38use crate::script_runtime::CanGc;
39
40#[dom_struct]
41pub(crate) struct ServiceWorkerContainer {
42    eventtarget: EventTarget,
43    controller: MutNullableDom<ServiceWorker>,
44
45    /// Pending results for
46    /// <https://w3c.github.io/ServiceWorker/#algorithms>
47    #[conditional_malloc_size_of]
48    pending_algorithm_results: DomRefCell<VecDeque<Rc<Promise>>>,
49
50    /// Handler of algorithm results.
51    #[no_trace]
52    callback: DomRefCell<Option<GenericCallback<ServiceWorkerAlgorithmResult>>>,
53}
54
55impl ServiceWorkerContainer {
56    fn new_inherited() -> ServiceWorkerContainer {
57        ServiceWorkerContainer {
58            eventtarget: EventTarget::new_inherited(),
59            controller: Default::default(),
60            pending_algorithm_results: Default::default(),
61            callback: Default::default(),
62        }
63    }
64
65    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<ServiceWorkerContainer> {
66        reflect_dom_object_with_cx(
67            Box::new(ServiceWorkerContainer::new_inherited()),
68            global,
69            cx,
70        )
71    }
72
73    /// <https://w3c.github.io/ServiceWorker/#reject-job-promise>
74    /// <https://w3c.github.io/ServiceWorker/#resolve-job-promise>
75    fn handle_job_result(&self, cx: &mut JSContext, result: JobResult, promise: Rc<Promise>) {
76        let global = self.global();
77        match result {
78            // <https://w3c.github.io/ServiceWorker/#reject-job-promise>
79            // Step 2.2: Queue a task, on equivalentJob’s client’s responsible event loop
80            // using the DOM manipulation task source,
81            // to reject equivalentJob’s job promise with a new exception with errorData,
82            // in equivalentJob’s client’s Realm.
83            // Note: we are in the task already.
84            JobResult::RejectPromise(error) => match error {
85                JobError::TypeError => {
86                    promise.reject_error_with_cx(
87                        cx,
88                        Error::Type(c"Failed to register a ServiceWorker".to_owned()),
89                    );
90                },
91                JobError::SecurityError => {
92                    promise.reject_error_with_cx(cx, Error::Security(None));
93                },
94            },
95            // <https://w3c.github.io/ServiceWorker/#resolve-job-promise>
96            JobResult::ResolvePromise(value) => {
97                match value {
98                    JobResultValue::Unregister(success) => {
99                        promise.resolve_native_with_cx(cx, &success);
100                    },
101                    JobResultValue::Register(value) => {
102                        let ServiceWorkerRegistrationInfo {
103                            id,
104                            installing_worker,
105                            waiting_worker,
106                            active_worker,
107                            storage_key: _,
108                            scope_url,
109                            script_url,
110                        } = value;
111                        // Step 2.2: If equivalentJob’s job type is either register or update,
112                        // set convertedValue to the result of getting the service worker registration object
113                        // that represents value in equivalentJob’s client.
114                        let registration = global.get_serviceworker_registration(
115                            &script_url,
116                            &scope_url,
117                            id,
118                            installing_worker,
119                            waiting_worker,
120                            active_worker,
121                            CanGc::from_cx(cx),
122                        );
123
124                        // TODO Step 2.3: Else, set convertedValue to value, in equivalentJob’s client’s Realm.
125
126                        // Step 2.4: Resolve equivalentJob’s job promise with convertedValue.
127                        promise.resolve_native_with_cx(cx, &*registration);
128                    },
129                }
130            },
131        }
132    }
133
134    /// Continuation of the parallel steps from
135    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkercontainer-getregistration>
136    fn handle_match_registration_result(
137        &self,
138        cx: &mut JSContext,
139        registration_info: Option<ServiceWorkerRegistrationInfo>,
140        promise: Rc<Promise>,
141    ) {
142        // Step 8.1 Let registration be the result of running Match Service Worker Registration given storage key and clientURL.
143        // Note: the `registration_info` argument is the result from the parallel algorithm run.
144
145        // Step 8.2: If registration is null, resolve promise with undefined and abort these steps.
146        let Some(info) = registration_info else {
147            promise.resolve_native_with_cx(cx, &());
148            return;
149        };
150
151        // Step 8.3: Resolve promise with the result of getting the service worker registration object
152        // that represents registration in promise’s relevant settings object.
153        let registration = self.global().get_serviceworker_registration(
154            &info.script_url,
155            &info.scope_url,
156            info.id,
157            info.installing_worker,
158            info.waiting_worker,
159            info.active_worker,
160            CanGc::from_cx(cx),
161        );
162        promise.resolve_native_with_cx(cx, &*registration);
163    }
164
165    fn handle_algorithm_result(&self, cx: &mut JSContext, result: ServiceWorkerAlgorithmResult) {
166        match result {
167            ServiceWorkerAlgorithmResult::Job(job_result) => {
168                let Some(promise) = self.pending_algorithm_results.borrow_mut().pop_front() else {
169                    debug_assert!(false, "No pending algorithm result.");
170                    return;
171                };
172                self.handle_job_result(cx, job_result, promise);
173            },
174            ServiceWorkerAlgorithmResult::MatchServiceWorkerRegistration(registration_info) => {
175                let Some(promise) = self.pending_algorithm_results.borrow_mut().pop_front() else {
176                    debug_assert!(false, "No pending algorithm result.");
177                    return;
178                };
179                self.handle_match_registration_result(cx, registration_info, promise);
180            },
181            ServiceWorkerAlgorithmResult::MessageFromWorker {
182                message,
183                source,
184                scope_url,
185                script_url,
186                origin,
187            } => {
188                // <https://w3c.github.io/ServiceWorker/#dom-client-postmessage-message-options>
189                // Add a task that runs the following steps to destination’s client message queue:
190                // Note: we are in the task.
191                // Step 4.5.2: Let source be the result of getting the service worker object
192                // that represents contextObject’s relevant global object’s service worker in targetClient.
193                let global = self.global();
194
195                // Note: spec uses a MesssageEvent, so it's unclear what to do with source.
196                // Perhaps an ExtendableMessageEvent should be used instead.
197                // See https://github.com/w3c/ServiceWorker/issues/1823
198                let _source =
199                    global.get_serviceworker(&script_url, &scope_url, source, CanGc::from_cx(cx));
200
201                // Step 4.5.4: Let messageClone be deserializeRecord.[[Deserialized]].
202                // Step 4.5.5: Let newPorts be a new frozen array consisting of all MessagePort objects
203                // in deserializeRecord.[[TransferredValues]], if any.
204                rooted!(&in(cx) let mut message_val = UndefinedValue());
205                if let Ok(ports) =
206                    structuredclone::read(cx, &global, message, message_val.handle_mut())
207                {
208                    // Step 4.5.6: Dispatch an event named message at destination, using MessageEvent, with its origin initialized to origin,
209                    // the source attribute initialized to source,
210                    // the data attribute initialized to messageClone, and the ports attribute initialized to newPorts.
211                    MessageEvent::dispatch_jsval(
212                        cx,
213                        self.upcast(),
214                        &global,
215                        message_val.handle(),
216                        Some(&origin.ascii_serialization()),
217                        None,
218                        ports,
219                    );
220                } else {
221                    error!("Failed to deserialize message ports in message from service worker.");
222                }
223            },
224        }
225    }
226
227    /// Setup the callback to the backend service, if this hasn't been done already.
228    fn get_or_setup_callback(
229        &self,
230        promise: Rc<Promise>,
231    ) -> GenericCallback<ServiceWorkerAlgorithmResult> {
232        self.pending_algorithm_results
233            .borrow_mut()
234            .push_back(promise);
235        if let Some(cb) = self.callback.borrow_mut().as_ref() {
236            return cb.clone();
237        }
238
239        let global = self.global();
240        let response_listener = Trusted::new(self);
241
242        let task_source = global
243            .task_manager()
244            .dom_manipulation_task_source()
245            .to_sendable();
246        let callback = GenericCallback::new(move |message| {
247            let response_listener = response_listener.clone();
248            let response = match message {
249                Ok(inner) => inner,
250                Err(err) => {
251                    return error!(
252                        "Error in Service worker algorithm result handlings {:?}.",
253                        err
254                    );
255                },
256            };
257            task_source.queue(task!(set_request_result_to_database: move |cx| {
258                let container = response_listener.root();
259                container.handle_algorithm_result(cx, response)
260            }));
261        })
262        .expect("Could not create callback");
263
264        *self.callback.borrow_mut() = Some(callback.clone());
265
266        callback
267    }
268
269    /// Continuation for
270    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkerregistration-unregister>
271    pub(crate) fn create_and_schedule_unregister_job(
272        &self,
273        cx: &mut JSContext,
274        storage_key: ImmutableOrigin,
275        scope: ServoUrl,
276        script_url: ServoUrl,
277        promise: Rc<Promise>,
278    ) {
279        let global = self.global();
280        let result_handler = self.get_or_setup_callback(promise);
281
282        // Step 3: Let job be the result of running Create Job with unregister,
283        // registration’s storage key, registration’s scope url, null, promise,
284        // and this’s relevant settings object.
285        let job = Job::create_job(
286            JobType::Unregister,
287            scope,
288            script_url,
289            result_handler,
290            global.creation_url(),
291            None,
292            storage_key,
293        );
294
295        // Step 4: Invoke Schedule Job with job.
296        if global
297            .script_to_constellation_chan()
298            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
299                ServiceWorkerAlgorithm::Unregister(job),
300            ))
301            .is_err()
302        {
303            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
304            self.pending_algorithm_results.borrow_mut().pop_back();
305
306            debug_assert!(
307                false,
308                "Failed to send Unregister algorithm message to the constellation."
309            );
310            self.handle_algorithm_result(
311                cx,
312                ServiceWorkerAlgorithmResult::Job(JobResult::RejectPromise(JobError::TypeError)),
313            );
314        }
315    }
316}
317
318impl ServiceWorkerContainerMethods<crate::DomTypeHolder> for ServiceWorkerContainer {
319    /// <https://w3c.github.io/ServiceWorker/#service-worker-container-controller-attribute>
320    fn GetController(&self) -> Option<DomRoot<ServiceWorker>> {
321        None
322    }
323
324    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkercontainer-register> - A
325    /// and <https://w3c.github.io/ServiceWorker/#start-register> - B
326    fn Register(
327        &self,
328        realm: &mut CurrentRealm,
329        script_url: USVString,
330        options: &RegistrationOptions,
331    ) -> Rc<Promise> {
332        // A: Step 2.
333        let global = self.global();
334
335        // A: Step 1
336        let promise = Promise::new_in_realm(realm);
337        let USVString(ref script_url) = script_url;
338
339        // A: Step 3
340        let api_base_url = global.api_base_url();
341        let script_url = match api_base_url.join(script_url) {
342            Ok(url) => url,
343            Err(_) => {
344                // B: Step 1
345                promise.reject_error_with_cx(realm, Error::Type(c"Invalid script URL".to_owned()));
346                return promise;
347            },
348        };
349
350        // A: Step 4-5
351        let scope = match options.scope {
352            Some(ref scope) => {
353                let USVString(inner_scope) = scope;
354                match api_base_url.join(inner_scope) {
355                    Ok(url) => url,
356                    Err(_) => {
357                        promise.reject_error_with_cx(
358                            realm,
359                            Error::Type(c"Invalid scope URL".to_owned()),
360                        );
361                        return promise;
362                    },
363                }
364            },
365            None => script_url.join("./").unwrap(),
366        };
367
368        // A: Step 6 -> invoke B.
369
370        // B: Step 3
371        match script_url.scheme() {
372            "https" | "http" => {},
373            _ => {
374                promise.reject_error_with_cx(
375                    realm,
376                    Error::Type(c"Only secure origins are allowed".to_owned()),
377                );
378                return promise;
379            },
380        }
381        // B: Step 4
382        if script_url.path().to_ascii_lowercase().contains("%2f") ||
383            script_url.path().to_ascii_lowercase().contains("%5c")
384        {
385            promise.reject_error_with_cx(
386                realm,
387                Error::Type(c"Script URL contains forbidden characters".to_owned()),
388            );
389            return promise;
390        }
391
392        // B: Step 6
393        match scope.scheme() {
394            "https" | "http" => {},
395            _ => {
396                promise.reject_error_with_cx(
397                    realm,
398                    Error::Type(c"Only secure origins are allowed".to_owned()),
399                );
400                return promise;
401            },
402        }
403        // B: Step 7
404        if scope.path().to_ascii_lowercase().contains("%2f") ||
405            scope.path().to_ascii_lowercase().contains("%5c")
406        {
407            promise.reject_error_with_cx(
408                realm,
409                Error::Type(c"Scope URL contains forbidden characters".to_owned()),
410            );
411            return promise;
412        }
413
414        let result_handler = self.get_or_setup_callback(promise.clone());
415
416        let scope_things =
417            ServiceWorkerRegistration::create_scope_things(&global, script_url.clone());
418
419        // B: Step 8 - 13
420
421        // Step 10: Let storage key be the result of running obtain a storage key given client.
422        let Some(storage_key) = global.obtain_storage_key() else {
423            promise.reject_error_with_cx(
424                realm,
425                Error::Type(c"Failed to obtain a storage key".to_owned()),
426            );
427            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
428            self.pending_algorithm_results.borrow_mut().pop_back();
429            return promise;
430        };
431
432        let job = Job::create_job(
433            JobType::Register,
434            scope,
435            script_url,
436            result_handler,
437            global.creation_url(),
438            Some(scope_things),
439            storage_key,
440        );
441
442        // B: Step 14: schedule job.
443        if global
444            .script_to_constellation_chan()
445            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
446                ServiceWorkerAlgorithm::StartRegister(job),
447            ))
448            .is_err()
449        {
450            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
451            self.pending_algorithm_results.borrow_mut().pop_back();
452            debug_assert!(
453                false,
454                "Failed to send StartRegister algorithm message to the constellation."
455            );
456            promise.reject_error_with_cx(
457                realm,
458                Error::Type(c"Failed to register a ServiceWorker".to_owned()),
459            );
460        }
461
462        // A: Step 7
463        promise
464    }
465
466    /// <https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration>
467    fn GetRegistration(&self, realm: &mut CurrentRealm, client_url: USVString) -> Rc<Promise> {
468        // Step 1: Let client be this’s service worker client.
469        let global = self.global();
470
471        // Step 7: Let promise be a new promise.
472        // Note: done here so it can be used to handle failure of the below steps.
473        let promise = Promise::new_in_realm(realm);
474
475        // Step 2: Let client storage key be the result of running obtain a storage key given client.
476        let Some(storage_key) = global.obtain_storage_key() else {
477            promise.reject_error_with_cx(
478                realm,
479                Error::Type(c"Failed to obtain a storage key".to_owned()),
480            );
481            return promise;
482        };
483
484        // Step 3: Let clientURL be the result of parsing clientURL with this’s relevant settings object’s API base URL.
485        let mut client_url = match global.api_base_url().join(&client_url.0) {
486            Ok(url) => url,
487            Err(_) => {
488                // Step 4: If clientURL is failure, return a promise rejected with a TypeError.
489                promise.reject_error_with_cx(
490                    realm,
491                    Error::Type(c"Failed to parse clientURL".to_owned()),
492                );
493                return promise;
494            },
495        };
496
497        // Step 5: Set clientURL’s fragment to null.
498        client_url.set_fragment(None);
499
500        // Step 6: If the origin of clientURL is not client’s origin, return a promise rejected with a "SecurityError" DOMException.
501        if &client_url.origin() != global.origin().immutable() {
502            promise.reject_error_with_cx(realm, Error::Security(None));
503            return promise;
504        }
505
506        let result_handler = self.get_or_setup_callback(promise.clone());
507
508        // Step 8: Run the following substeps in parallel:
509        // Note: continues in parallel in the service worker manager,
510        // by way of the constellation.
511        if global
512            .script_to_constellation_chan()
513            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
514                ServiceWorkerAlgorithm::MatchServiceWorkerRegistration {
515                    client_url,
516                    storage_key,
517                    result_handler,
518                },
519            ))
520            .is_err()
521        {
522            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
523            self.pending_algorithm_results.borrow_mut().pop_back();
524            promise.reject_error_with_cx(
525                realm,
526                Error::Type(c"Failed to send MatchServiceWorkerRegistration algorithm".to_owned()),
527            );
528        }
529
530        // Step 9: Return promise.
531        promise
532    }
533}