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