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::default::Default;
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use js::realm::CurrentRealm;
10use script_bindings::reflector::reflect_dom_object;
11use servo_base::generic_channel::GenericCallback;
12use servo_constellation_traits::{
13    Job, JobError, JobResult, JobResultValue, JobType, ScriptToConstellationMessage,
14};
15
16use crate::dom::bindings::codegen::Bindings::ServiceWorkerContainerBinding::{
17    RegistrationOptions, ServiceWorkerContainerMethods,
18};
19use crate::dom::bindings::error::Error;
20use crate::dom::bindings::refcounted::TrustedPromise;
21use crate::dom::bindings::reflector::DomGlobal;
22use crate::dom::bindings::root::{DomRoot, MutNullableDom};
23use crate::dom::bindings::str::USVString;
24use crate::dom::eventtarget::EventTarget;
25use crate::dom::globalscope::GlobalScope;
26use crate::dom::promise::Promise;
27use crate::dom::serviceworker::ServiceWorker;
28use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
29use crate::realms::{InRealm, enter_realm};
30use crate::script_runtime::CanGc;
31use crate::task_source::SendableTaskSource;
32
33#[dom_struct]
34pub(crate) struct ServiceWorkerContainer {
35    eventtarget: EventTarget,
36    controller: MutNullableDom<ServiceWorker>,
37}
38
39impl ServiceWorkerContainer {
40    fn new_inherited() -> ServiceWorkerContainer {
41        ServiceWorkerContainer {
42            eventtarget: EventTarget::new_inherited(),
43            controller: Default::default(),
44        }
45    }
46
47    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<ServiceWorkerContainer> {
48        reflect_dom_object(
49            Box::new(ServiceWorkerContainer::new_inherited()),
50            global,
51            can_gc,
52        )
53    }
54}
55
56impl ServiceWorkerContainerMethods<crate::DomTypeHolder> for ServiceWorkerContainer {
57    /// <https://w3c.github.io/ServiceWorker/#service-worker-container-controller-attribute>
58    fn GetController(&self) -> Option<DomRoot<ServiceWorker>> {
59        None
60    }
61
62    /// <https://w3c.github.io/ServiceWorker/#dom-serviceworkercontainer-register> - A
63    /// and <https://w3c.github.io/ServiceWorker/#start-register> - B
64    fn Register(
65        &self,
66        realm: &mut CurrentRealm,
67        script_url: USVString,
68        options: &RegistrationOptions,
69    ) -> Rc<Promise> {
70        let can_gc = CanGc::from_cx(realm);
71        // A: Step 2.
72        let global = self.global();
73
74        // A: Step 1
75        let promise = Promise::new_in_current_realm(InRealm::already(&realm.into()), can_gc);
76        let USVString(ref script_url) = script_url;
77
78        // A: Step 3
79        let api_base_url = global.api_base_url();
80        let script_url = match api_base_url.join(script_url) {
81            Ok(url) => url,
82            Err(_) => {
83                // B: Step 1
84                promise.reject_error(Error::Type(c"Invalid script URL".to_owned()), can_gc);
85                return promise;
86            },
87        };
88
89        // A: Step 4-5
90        let scope = match options.scope {
91            Some(ref scope) => {
92                let USVString(inner_scope) = scope;
93                match api_base_url.join(inner_scope) {
94                    Ok(url) => url,
95                    Err(_) => {
96                        promise.reject_error(Error::Type(c"Invalid scope URL".to_owned()), can_gc);
97                        return promise;
98                    },
99                }
100            },
101            None => script_url.join("./").unwrap(),
102        };
103
104        // A: Step 6 -> invoke B.
105
106        // B: Step 3
107        match script_url.scheme() {
108            "https" | "http" => {},
109            _ => {
110                promise.reject_error(
111                    Error::Type(c"Only secure origins are allowed".to_owned()),
112                    can_gc,
113                );
114                return promise;
115            },
116        }
117        // B: Step 4
118        if script_url.path().to_ascii_lowercase().contains("%2f") ||
119            script_url.path().to_ascii_lowercase().contains("%5c")
120        {
121            promise.reject_error(
122                Error::Type(c"Script URL contains forbidden characters".to_owned()),
123                can_gc,
124            );
125            return promise;
126        }
127
128        // B: Step 6
129        match scope.scheme() {
130            "https" | "http" => {},
131            _ => {
132                promise.reject_error(
133                    Error::Type(c"Only secure origins are allowed".to_owned()),
134                    can_gc,
135                );
136                return promise;
137            },
138        }
139        // B: Step 7
140        if scope.path().to_ascii_lowercase().contains("%2f") ||
141            scope.path().to_ascii_lowercase().contains("%5c")
142        {
143            promise.reject_error(
144                Error::Type(c"Scope URL contains forbidden characters".to_owned()),
145                can_gc,
146            );
147            return promise;
148        }
149
150        // Setup the callback for reject/resolve of the promise,
151        // from steps running "in-parallel" from here in the serviceworker manager.
152        let mut handler = RegisterJobResultHandler {
153            trusted_promise: Some(TrustedPromise::new(promise.clone())),
154            task_source: global.task_manager().dom_manipulation_task_source().into(),
155        };
156
157        let result_handler = GenericCallback::new(move |message| match message {
158            Ok(msg) => handler.handle(msg),
159            Err(err) => warn!("Error receiving a JobResult: {:?}", err),
160        })
161        .expect("Failed to create callback");
162
163        let scope_things =
164            ServiceWorkerRegistration::create_scope_things(&global, script_url.clone());
165
166        // B: Step 8 - 13
167
168        // Step 10: Let storage key be the result of running obtain a storage key given client.
169        let Some(storage_key) = global.obtain_storage_key() else {
170            promise.reject_error(
171                Error::Type(c"Failed to obtain a storage key".to_owned()),
172                can_gc,
173            );
174            return promise;
175        };
176
177        let job = Job::create_job(
178            JobType::Register,
179            scope,
180            script_url,
181            result_handler,
182            global.creation_url(),
183            Some(scope_things),
184            storage_key,
185        );
186
187        // B: Step 14: schedule job.
188        let _ = global
189            .script_to_constellation_chan()
190            .send(ScriptToConstellationMessage::ScheduleJob(job));
191
192        // A: Step 7
193        promise
194    }
195}
196
197/// Callback for resolve/reject job promise for Register.
198/// <https://w3c.github.io/ServiceWorker/#register>
199struct RegisterJobResultHandler {
200    trusted_promise: Option<TrustedPromise>,
201    task_source: SendableTaskSource,
202}
203
204impl RegisterJobResultHandler {
205    /// <https://w3c.github.io/ServiceWorker/#reject-job-promise>
206    /// <https://w3c.github.io/ServiceWorker/#resolve-job-promise>
207    /// Handle a result to either resolve or reject the register job promise.
208    pub(crate) fn handle(&mut self, result: JobResult) {
209        match result {
210            JobResult::RejectPromise(error) => {
211                let promise = self
212                    .trusted_promise
213                    .take()
214                    .expect("No promise to resolve for SW Register job.");
215
216                // Step 1
217                self.task_source
218                    .queue(task!(reject_promise_with_security_error: move || {
219                        let promise = promise.root();
220                        let _ac = enter_realm(&*promise.global());
221                        match error {
222                            JobError::TypeError => {
223                                promise.reject_error(
224                                    Error::Type(c"Failed to register a ServiceWorker".to_owned()),
225                                    CanGc::deprecated_note(),
226                                );
227                            },
228                            JobError::SecurityError => {
229                                promise.reject_error(Error::Security(None), CanGc::deprecated_note());
230                            },
231                        }
232
233                    }));
234
235                // TODO: step 2, handle equivalent jobs.
236            },
237            JobResult::ResolvePromise(job, value) => {
238                let promise = self
239                    .trusted_promise
240                    .take()
241                    .expect("No promise to resolve for SW Register job.");
242
243                // Step 1
244                self.task_source.queue(task!(resolve_promise: move || {
245                    let promise = promise.root();
246                    let global = promise.global();
247                    let _ac = enter_realm(&*global);
248
249                    // Step 1.1
250                    let JobResultValue::Registration {
251                        id,
252                        installing_worker,
253                        waiting_worker,
254                        active_worker,
255                    } = value;
256
257                    // Step 1.2 (Job type is "register").
258                    let registration = global.get_serviceworker_registration(
259                        &job.script_url,
260                        &job.scope_url,
261                        id,
262                        installing_worker,
263                        waiting_worker,
264                        active_worker,
265                        CanGc::deprecated_note()
266                    );
267
268                    // Step 1.4
269                    promise.resolve_native(&*registration, CanGc::deprecated_note());
270                }));
271
272                // TODO: step 2, handle equivalent jobs.
273            },
274        }
275    }
276}