Skip to main content

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