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