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;
38
39#[dom_struct]
40pub(crate) struct ServiceWorkerContainer {
41    eventtarget: EventTarget,
42    controller: MutNullableDom<ServiceWorker>,
43
44    /// Pending results for
45    /// <https://w3c.github.io/ServiceWorker/#algorithms>
46    #[conditional_malloc_size_of]
47    pending_algorithm_results: DomRefCell<VecDeque<Rc<Promise>>>,
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_with_cx(
66            Box::new(ServiceWorkerContainer::new_inherited()),
67            global,
68            cx,
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: Rc<Promise>) {
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: Rc<Promise>,
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 Some(promise) = self.pending_algorithm_results.borrow_mut().pop_front() else {
168                    debug_assert!(false, "No pending algorithm result.");
169                    return;
170                };
171                self.handle_job_result(cx, job_result, promise);
172            },
173            ServiceWorkerAlgorithmResult::MatchServiceWorkerRegistration(registration_info) => {
174                let Some(promise) = self.pending_algorithm_results.borrow_mut().pop_front() else {
175                    debug_assert!(false, "No pending algorithm result.");
176                    return;
177                };
178                self.handle_match_registration_result(cx, registration_info, promise);
179            },
180            ServiceWorkerAlgorithmResult::MessageFromWorker {
181                message,
182                source,
183                scope_url,
184                script_url,
185                origin,
186            } => {
187                // <https://w3c.github.io/ServiceWorker/#dom-client-postmessage-message-options>
188                // Add a task that runs the following steps to destination’s client message queue:
189                // Note: we are in the task.
190                // Step 4.5.2: Let source be the result of getting the service worker object
191                // that represents contextObject’s relevant global object’s service worker in targetClient.
192                let global = self.global();
193
194                // Note: spec uses a MesssageEvent, so it's unclear what to do with source.
195                // Perhaps an ExtendableMessageEvent should be used instead.
196                // See https://github.com/w3c/ServiceWorker/issues/1823
197                let _source = global.get_serviceworker(cx, &script_url, &scope_url, source);
198
199                // Step 4.5.4: Let messageClone be deserializeRecord.[[Deserialized]].
200                // Step 4.5.5: Let newPorts be a new frozen array consisting of all MessagePort objects
201                // in deserializeRecord.[[TransferredValues]], if any.
202                rooted!(&in(cx) let mut message_val = UndefinedValue());
203                if let Ok(ports) =
204                    structuredclone::read(cx, &global, message, message_val.handle_mut())
205                {
206                    // Step 4.5.6: Dispatch an event named message at destination, using MessageEvent, with its origin initialized to origin,
207                    // the source attribute initialized to source,
208                    // the data attribute initialized to messageClone, and the ports attribute initialized to newPorts.
209                    MessageEvent::dispatch_jsval(
210                        cx,
211                        self.upcast(),
212                        &global,
213                        message_val.handle(),
214                        Some(&origin.ascii_serialization()),
215                        None,
216                        ports,
217                    );
218                } else {
219                    error!("Failed to deserialize message ports in message from service worker.");
220                }
221            },
222        }
223    }
224
225    /// Setup the callback to the backend service, if this hasn't been done already.
226    fn get_or_setup_callback(
227        &self,
228        promise: Rc<Promise>,
229    ) -> GenericCallback<ServiceWorkerAlgorithmResult> {
230        self.pending_algorithm_results
231            .borrow_mut()
232            .push_back(promise);
233        if let Some(cb) = self.callback.borrow_mut().as_ref() {
234            return cb.clone();
235        }
236
237        let global = self.global();
238        let response_listener = Trusted::new(self);
239
240        let task_source = global
241            .task_manager()
242            .dom_manipulation_task_source()
243            .to_sendable();
244        let callback = GenericCallback::new(move |message| {
245            let response_listener = response_listener.clone();
246            let response = match message {
247                Ok(inner) => inner,
248                Err(err) => {
249                    return error!(
250                        "Error in Service worker algorithm result handlings {:?}.",
251                        err
252                    );
253                },
254            };
255            task_source.queue(task!(set_request_result_to_database: move |cx| {
256                let container = response_listener.root();
257                container.handle_algorithm_result(cx, response)
258            }));
259        })
260        .expect("Could not create callback");
261
262        *self.callback.borrow_mut() = Some(callback.clone());
263
264        callback
265    }
266
267    /// Continuation for
268    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkerregistration-unregister>
269    pub(crate) fn create_and_schedule_unregister_job(
270        &self,
271        cx: &mut JSContext,
272        storage_key: ImmutableOrigin,
273        scope: ServoUrl,
274        script_url: ServoUrl,
275        promise: Rc<Promise>,
276    ) {
277        let global = self.global();
278        let result_handler = self.get_or_setup_callback(promise);
279
280        // Step 3: Let job be the result of running Create Job with unregister,
281        // registration’s storage key, registration’s scope url, null, promise,
282        // and this’s relevant settings object.
283        let job = Job::create_job(
284            JobType::Unregister,
285            scope,
286            script_url,
287            result_handler,
288            global.creation_url(),
289            None,
290            storage_key,
291        );
292
293        // Step 4: Invoke Schedule Job with job.
294        if global
295            .script_to_constellation_chan()
296            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
297                ServiceWorkerAlgorithm::Unregister(job),
298            ))
299            .is_err()
300        {
301            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
302            self.pending_algorithm_results.borrow_mut().pop_back();
303
304            debug_assert!(
305                false,
306                "Failed to send Unregister algorithm message to the constellation."
307            );
308            self.handle_algorithm_result(
309                cx,
310                ServiceWorkerAlgorithmResult::Job(JobResult::RejectPromise(JobError::TypeError)),
311            );
312        }
313    }
314}
315
316impl ServiceWorkerContainerMethods<crate::DomTypeHolder> for ServiceWorkerContainer {
317    /// <https://w3c.github.io/ServiceWorker/#service-worker-container-controller-attribute>
318    fn GetController(&self) -> Option<DomRoot<ServiceWorker>> {
319        None
320    }
321
322    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkercontainer-register> - A
323    /// and <https://w3c.github.io/ServiceWorker/#start-register> - B
324    fn Register(
325        &self,
326        realm: &mut CurrentRealm,
327        script_url: USVString,
328        options: &RegistrationOptions,
329    ) -> Rc<Promise> {
330        // A: Step 2.
331        let global = self.global();
332
333        // A: Step 1
334        let promise = Promise::new_in_realm(realm);
335        let USVString(ref script_url) = script_url;
336
337        // A: Step 3
338        let api_base_url = global.api_base_url();
339        let script_url = match api_base_url.join(script_url) {
340            Ok(url) => url,
341            Err(_) => {
342                // B: Step 1
343                promise.reject_error(realm, Error::Type(c"Invalid script URL".to_owned()));
344                return promise;
345            },
346        };
347
348        // A: Step 4-5
349        let scope = match options.scope {
350            Some(ref scope) => {
351                let USVString(inner_scope) = scope;
352                match api_base_url.join(inner_scope) {
353                    Ok(url) => url,
354                    Err(_) => {
355                        promise.reject_error(realm, Error::Type(c"Invalid scope URL".to_owned()));
356                        return promise;
357                    },
358                }
359            },
360            None => script_url.join("./").unwrap(),
361        };
362
363        // A: Step 6 -> invoke B.
364
365        // B: Step 3
366        match script_url.scheme() {
367            "https" | "http" => {},
368            _ => {
369                promise.reject_error(
370                    realm,
371                    Error::Type(c"Only secure origins are allowed".to_owned()),
372                );
373                return promise;
374            },
375        }
376        // B: Step 4
377        if script_url.path().to_ascii_lowercase().contains("%2f") ||
378            script_url.path().to_ascii_lowercase().contains("%5c")
379        {
380            promise.reject_error(
381                realm,
382                Error::Type(c"Script URL contains forbidden characters".to_owned()),
383            );
384            return promise;
385        }
386
387        // B: Step 6
388        match scope.scheme() {
389            "https" | "http" => {},
390            _ => {
391                promise.reject_error(
392                    realm,
393                    Error::Type(c"Only secure origins are allowed".to_owned()),
394                );
395                return promise;
396            },
397        }
398        // B: Step 7
399        if scope.path().to_ascii_lowercase().contains("%2f") ||
400            scope.path().to_ascii_lowercase().contains("%5c")
401        {
402            promise.reject_error(
403                realm,
404                Error::Type(c"Scope URL contains forbidden characters".to_owned()),
405            );
406            return promise;
407        }
408
409        let result_handler = self.get_or_setup_callback(promise.clone());
410
411        let scope_things =
412            ServiceWorkerRegistration::create_scope_things(&global, script_url.clone());
413
414        // B: Step 8 - 13
415
416        // Step 10: Let storage key be the result of running obtain a storage key given client.
417        let Some(storage_key) = global.obtain_storage_key() else {
418            promise.reject_error(
419                realm,
420                Error::Type(c"Failed to obtain a storage key".to_owned()),
421            );
422            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
423            self.pending_algorithm_results.borrow_mut().pop_back();
424            return promise;
425        };
426
427        let job = Job::create_job(
428            JobType::Register,
429            scope,
430            script_url,
431            result_handler,
432            global.creation_url(),
433            Some(scope_things),
434            storage_key,
435        );
436
437        // B: Step 14: schedule job.
438        if global
439            .script_to_constellation_chan()
440            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
441                ServiceWorkerAlgorithm::StartRegister(job),
442            ))
443            .is_err()
444        {
445            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
446            self.pending_algorithm_results.borrow_mut().pop_back();
447            debug_assert!(
448                false,
449                "Failed to send StartRegister algorithm message to the constellation."
450            );
451            promise.reject_error(
452                realm,
453                Error::Type(c"Failed to register a ServiceWorker".to_owned()),
454            );
455        }
456
457        // A: Step 7
458        promise
459    }
460
461    /// <https://w3c.github.io/ServiceWorker/#navigator-service-worker-getRegistration>
462    fn GetRegistration(&self, realm: &mut CurrentRealm, client_url: USVString) -> Rc<Promise> {
463        // Step 1: Let client be this’s service worker client.
464        let global = self.global();
465
466        // Step 7: Let promise be a new promise.
467        // Note: done here so it can be used to handle failure of the below steps.
468        let promise = Promise::new_in_realm(realm);
469
470        // Step 2: Let client storage key be the result of running obtain a storage key given client.
471        let Some(storage_key) = global.obtain_storage_key() else {
472            promise.reject_error(
473                realm,
474                Error::Type(c"Failed to obtain a storage key".to_owned()),
475            );
476            return promise;
477        };
478
479        // Step 3: Let clientURL be the result of parsing clientURL with this’s relevant settings object’s API base URL.
480        let mut client_url = match global.api_base_url().join(&client_url.0) {
481            Ok(url) => url,
482            Err(_) => {
483                // Step 4: If clientURL is failure, return a promise rejected with a TypeError.
484                promise.reject_error(realm, Error::Type(c"Failed to parse clientURL".to_owned()));
485                return promise;
486            },
487        };
488
489        // Step 5: Set clientURL’s fragment to null.
490        client_url.set_fragment(None);
491
492        // Step 6: If the origin of clientURL is not client’s origin, return a promise rejected with a "SecurityError" DOMException.
493        if &client_url.origin() != global.origin().immutable() {
494            promise.reject_error(realm, Error::Security(None));
495            return promise;
496        }
497
498        let result_handler = self.get_or_setup_callback(promise.clone());
499
500        // Step 8: Run the following substeps in parallel:
501        // Note: continues in parallel in the service worker manager,
502        // by way of the constellation.
503        if global
504            .script_to_constellation_chan()
505            .send(ScriptToConstellationMessage::ServiceWorkerAlgorithm(
506                ServiceWorkerAlgorithm::MatchServiceWorkerRegistration {
507                    client_url,
508                    storage_key,
509                    result_handler,
510                },
511            ))
512            .is_err()
513        {
514            // Note: pop the promise we just pushed, since we will not get a result back to handle it.
515            self.pending_algorithm_results.borrow_mut().pop_back();
516            promise.reject_error(
517                realm,
518                Error::Type(c"Failed to send MatchServiceWorkerRegistration algorithm".to_owned()),
519            );
520        }
521
522        // Step 9: Return promise.
523        promise
524    }
525}