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;
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(
87                        cx,
88                        Error::Type(c"Failed to register a ServiceWorker".to_owned()),
89                    );
90                },
91                JobError::SecurityError => {
92                    promise.reject_error(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(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(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(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(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(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(realm, Error::Type(c"Invalid scope URL".to_owned()));
358                        return promise;
359                    },
360                }
361            },
362            None => script_url.join("./").unwrap(),
363        };
364
365        // A: Step 6 -> invoke B.
366
367        // B: Step 3
368        match script_url.scheme() {
369            "https" | "http" => {},
370            _ => {
371                promise.reject_error(
372                    realm,
373                    Error::Type(c"Only secure origins are allowed".to_owned()),
374                );
375                return promise;
376            },
377        }
378        // B: Step 4
379        if script_url.path().to_ascii_lowercase().contains("%2f") ||
380            script_url.path().to_ascii_lowercase().contains("%5c")
381        {
382            promise.reject_error(
383                realm,
384                Error::Type(c"Script URL contains forbidden characters".to_owned()),
385            );
386            return promise;
387        }
388
389        // B: Step 6
390        match scope.scheme() {
391            "https" | "http" => {},
392            _ => {
393                promise.reject_error(
394                    realm,
395                    Error::Type(c"Only secure origins are allowed".to_owned()),
396                );
397                return promise;
398            },
399        }
400        // B: Step 7
401        if scope.path().to_ascii_lowercase().contains("%2f") ||
402            scope.path().to_ascii_lowercase().contains("%5c")
403        {
404            promise.reject_error(
405                realm,
406                Error::Type(c"Scope URL contains forbidden characters".to_owned()),
407            );
408            return promise;
409        }
410
411        let result_handler = self.get_or_setup_callback(promise.clone());
412
413        let scope_things =
414            ServiceWorkerRegistration::create_scope_things(&global, script_url.clone());
415
416        // B: Step 8 - 13
417
418        // Step 10: Let storage key be the result of running obtain a storage key given client.
419        let Some(storage_key) = global.obtain_storage_key() else {
420            promise.reject_error(
421                realm,
422                Error::Type(c"Failed to obtain a storage key".to_owned()),
423            );
424            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
425            self.pending_algorithm_results.borrow_mut().pop_back();
426            return promise;
427        };
428
429        let job = Job::create_job(
430            JobType::Register,
431            scope,
432            script_url,
433            result_handler,
434            global.creation_url(),
435            Some(scope_things),
436            storage_key,
437        );
438
439        // B: Step 14: schedule job.
440        if global
441            .script_to_constellation_chan()
442            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
443                ServiceWorkerAlgorithm::StartRegister(job),
444            ))
445            .is_err()
446        {
447            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
448            self.pending_algorithm_results.borrow_mut().pop_back();
449            debug_assert!(
450                false,
451                "Failed to send StartRegister algorithm message to the constellation."
452            );
453            promise.reject_error(
454                realm,
455                Error::Type(c"Failed to register a ServiceWorker".to_owned()),
456            );
457        }
458
459        // A: Step 7
460        promise
461    }
462
463    /// <https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration>
464    fn GetRegistration(&self, realm: &mut CurrentRealm, client_url: USVString) -> Rc<Promise> {
465        // Step 1: Let client be this’s service worker client.
466        let global = self.global();
467
468        // Step 7: Let promise be a new promise.
469        // Note: done here so it can be used to handle failure of the below steps.
470        let promise = Promise::new_in_realm(realm);
471
472        // Step 2: Let client storage key be the result of running obtain a storage key given client.
473        let Some(storage_key) = global.obtain_storage_key() else {
474            promise.reject_error(
475                realm,
476                Error::Type(c"Failed to obtain a storage key".to_owned()),
477            );
478            return promise;
479        };
480
481        // Step 3: Let clientURL be the result of parsing clientURL with this’s relevant settings object’s API base URL.
482        let mut client_url = match global.api_base_url().join(&client_url.0) {
483            Ok(url) => url,
484            Err(_) => {
485                // Step 4: If clientURL is failure, return a promise rejected with a TypeError.
486                promise.reject_error(realm, Error::Type(c"Failed to parse clientURL".to_owned()));
487                return promise;
488            },
489        };
490
491        // Step 5: Set clientURL’s fragment to null.
492        client_url.set_fragment(None);
493
494        // Step 6: If the origin of clientURL is not client’s origin, return a promise rejected with a "SecurityError" DOMException.
495        if &client_url.origin() != global.origin().immutable() {
496            promise.reject_error(realm, Error::Security(None));
497            return promise;
498        }
499
500        let result_handler = self.get_or_setup_callback(promise.clone());
501
502        // Step 8: Run the following substeps in parallel:
503        // Note: continues in parallel in the service worker manager,
504        // by way of the constellation.
505        if global
506            .script_to_constellation_chan()
507            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
508                ServiceWorkerAlgorithm::MatchServiceWorkerRegistration {
509                    client_url,
510                    storage_key,
511                    result_handler,
512                },
513            ))
514            .is_err()
515        {
516            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
517            self.pending_algorithm_results.borrow_mut().pop_back();
518            promise.reject_error(
519                realm,
520                Error::Type(c"Failed to send MatchServiceWorkerRegistration algorithm".to_owned()),
521            );
522        }
523
524        // Step 9: Return promise.
525        promise
526    }
527}