1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::IpcSend;
10use base::id::CookieStoreId;
11use cookie::{Cookie, SameSite};
12use dom_struct::dom_struct;
13use hyper_serde::Serde;
14use ipc_channel::ipc;
15use ipc_channel::ipc::IpcSender;
16use ipc_channel::router::ROUTER;
17use itertools::Itertools;
18use js::jsval::NullValue;
19use net_traits::CookieSource::NonHTTP;
20use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
21use script_bindings::script_runtime::CanGc;
22use servo_url::ServoUrl;
23
24use crate::dom::bindings::cell::DomRefCell;
25use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
26 CookieInit, CookieListItem, CookieStoreDeleteOptions, CookieStoreGetOptions, CookieStoreMethods,
27};
28use crate::dom::bindings::error::Error;
29use crate::dom::bindings::refcounted::Trusted;
30use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
31use crate::dom::bindings::root::DomRoot;
32use crate::dom::bindings::str::USVString;
33use crate::dom::eventtarget::EventTarget;
34use crate::dom::globalscope::GlobalScope;
35use crate::dom::promise::Promise;
36use crate::dom::window::Window;
37use crate::task_source::SendableTaskSource;
38
39#[dom_struct]
44pub(crate) struct CookieStore {
45 eventtarget: EventTarget,
46 #[conditional_malloc_size_of]
47 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
48 #[no_trace]
50 store_id: CookieStoreId,
51 #[ignore_malloc_size_of = "Channels are hard"]
52 #[no_trace]
53 unregister_channel: IpcSender<CoreResourceMsg>,
54}
55
56struct CookieListener {
57 task_source: SendableTaskSource,
59 context: Trusted<CookieStore>,
60}
61
62impl CookieListener {
63 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
64 let context = self.context.clone();
65 self.task_source.queue(task!(cookie_message: move || {
66 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
67 warn!("No promise exists for cookie store response");
68 return;
69 };
70 match message.data {
71 CookieData::Get(cookie) => {
72 if let Some(cookie) = cookie {
75 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
77 } else {
78 promise.resolve_native(&NullValue(), CanGc::note());
80 }
81 },
82 CookieData::GetAll(cookies) => {
83 promise.resolve_native(
85 &cookies
86 .into_iter()
87 .map(|cookie| cookie_to_list_item(cookie.0))
88 .collect_vec(),
89 CanGc::note());
90 },
91 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
92 promise.resolve_native(&(), CanGc::note());
93 }
94 }
95 }));
96 }
97}
98
99impl CookieStore {
100 fn new_inherited(unregister_channel: IpcSender<CoreResourceMsg>) -> CookieStore {
101 CookieStore {
102 eventtarget: EventTarget::new_inherited(),
103 in_flight: Default::default(),
104 store_id: CookieStoreId::new(),
105 unregister_channel,
106 }
107 }
108
109 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
110 let store = reflect_dom_object(
111 Box::new(CookieStore::new_inherited(
112 global.resource_threads().core_thread.clone(),
113 )),
114 global,
115 can_gc,
116 );
117 store.setup_route();
118 store
119 }
120
121 fn setup_route(&self) {
122 let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
123
124 let context = Trusted::new(self);
125 let cs_listener = CookieListener {
126 task_source: self
127 .global()
128 .task_manager()
129 .dom_manipulation_task_source()
130 .to_sendable(),
131 context,
132 };
133
134 ROUTER.add_typed_route(
135 cookie_receiver,
136 Box::new(move |message| match message {
137 Ok(msg) => cs_listener.handle(msg),
138 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
139 }),
140 );
141
142 let res = self
143 .global()
144 .resource_threads()
145 .send(CoreResourceMsg::NewCookieListener(
146 self.store_id,
147 cookie_sender,
148 self.global().creation_url().clone(),
149 ));
150 if res.is_err() {
151 error!("Failed to send cookiestore message to resource threads");
152 }
153 }
154}
155
156fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
158 CookieListItem {
161 name: Some(cookie.name().to_string().into()),
163
164 value: Some(cookie.value().to_string().into()),
166 }
167}
168
169impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
170 fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
172 let global = self.global();
174
175 let origin = global.origin();
177
178 let p = Promise::new(&global, can_gc);
180
181 if !origin.is_tuple() {
183 p.reject_error(Error::Security, can_gc);
184 return p;
185 }
186
187 let creation_url = global.creation_url();
189
190 let res = self
192 .global()
193 .resource_threads()
194 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
195 self.store_id,
196 creation_url.clone(),
197 Some(name.to_string()),
198 ));
199 if res.is_err() {
200 error!("Failed to send cookiestore message to resource threads");
201 } else {
202 self.in_flight.borrow_mut().push_back(p.clone());
203 }
204
205 p
207 }
208
209 fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
211 let global = self.global();
213
214 let origin = global.origin();
216
217 let p = Promise::new(&global, can_gc);
219
220 if !origin.is_tuple() {
222 p.reject_error(Error::Security, can_gc);
223 return p;
224 }
225
226 let creation_url = global.creation_url();
228
229 if options.url.is_none() && options.name.is_none() {
232 p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
233 return p;
234 }
235
236 let mut final_url = creation_url.clone();
237
238 if let Some(get_url) = &options.url {
240 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
242
243 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
246 if parsed_url
247 .as_ref()
248 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
249 {
250 p.reject_error(
251 Error::Type("URL does not match context".to_string()),
252 can_gc,
253 );
254 return p;
255 }
256 }
257
258 if parsed_url
261 .as_ref()
262 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
263 {
264 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
265 return p;
266 }
267
268 if let Ok(url) = parsed_url {
270 final_url = url;
271 }
272 }
273
274 let res = self
276 .global()
277 .resource_threads()
278 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
279 self.store_id,
280 final_url.clone(),
281 options.name.clone().map(|val| val.0),
282 ));
283 if res.is_err() {
284 error!("Failed to send cookiestore message to resource threads");
285 } else {
286 self.in_flight.borrow_mut().push_back(p.clone());
287 }
288
289 p
290 }
291
292 fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
294 let global = self.global();
296
297 let origin = global.origin();
299
300 let p = Promise::new(&global, can_gc);
302
303 if !origin.is_tuple() {
305 p.reject_error(Error::Security, can_gc);
306 return p;
307 }
308 let creation_url = global.creation_url();
310
311 let res =
313 self.global()
314 .resource_threads()
315 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
316 self.store_id,
317 creation_url.clone(),
318 Some(name.to_string()),
319 ));
320 if res.is_err() {
321 error!("Failed to send cookiestore message to resource threads");
322 } else {
323 self.in_flight.borrow_mut().push_back(p.clone());
324 }
325
326 p
328 }
329
330 fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
332 let global = self.global();
334
335 let origin = global.origin();
337
338 let p = Promise::new(&global, can_gc);
340
341 if !origin.is_tuple() {
343 p.reject_error(Error::Security, can_gc);
344 return p;
345 }
346
347 let creation_url = global.creation_url();
349
350 let mut final_url = creation_url.clone();
351
352 if let Some(get_url) = &options.url {
354 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
356
357 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
360 if parsed_url
361 .as_ref()
362 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
363 {
364 p.reject_error(
365 Error::Type("URL does not match context".to_string()),
366 can_gc,
367 );
368 return p;
369 }
370 }
371
372 if parsed_url
375 .as_ref()
376 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
377 {
378 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
379 return p;
380 }
381
382 if let Ok(url) = parsed_url {
384 final_url = url;
385 }
386 }
387
388 let res =
390 self.global()
391 .resource_threads()
392 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
393 self.store_id,
394 final_url.clone(),
395 options.name.clone().map(|val| val.0),
396 ));
397 if res.is_err() {
398 error!("Failed to send cookiestore message to resource threads");
399 } else {
400 self.in_flight.borrow_mut().push_back(p.clone());
401 }
402
403 p
405 }
406
407 fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
409 let global = self.global();
411
412 let origin = global.origin();
414
415 let p = Promise::new(&global, can_gc);
417
418 if !origin.is_tuple() {
420 p.reject_error(Error::Security, can_gc);
421 return p;
422 }
423
424 let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
430 .path("/")
431 .secure(true)
432 .same_site(SameSite::Strict)
433 .partitioned(false);
434 let res = self
439 .global()
440 .resource_threads()
441 .send(CoreResourceMsg::SetCookieForUrlAsync(
442 self.store_id,
443 self.global().creation_url().clone(),
444 Serde(cookie.build()),
445 NonHTTP,
446 ));
447 if res.is_err() {
448 error!("Failed to send cookiestore message to resource threads");
449 } else {
450 self.in_flight.borrow_mut().push_back(p.clone());
451 }
452
453 p
455 }
456
457 fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
459 let global = self.global();
461
462 let origin = global.origin();
464
465 let p = Promise::new(&global, can_gc);
467
468 if !origin.is_tuple() {
470 p.reject_error(Error::Security, can_gc);
471 return p;
472 }
473
474 let creation_url = global.creation_url();
476
477 let cookie = Cookie::build((
480 Cow::Owned(options.name.to_string()),
481 Cow::Owned(options.value.to_string()),
482 ));
483 let res = self
488 .global()
489 .resource_threads()
490 .send(CoreResourceMsg::SetCookieForUrlAsync(
491 self.store_id,
492 creation_url.clone(),
493 Serde(cookie.build()),
494 NonHTTP,
495 ));
496 if res.is_err() {
497 error!("Failed to send cookiestore message to resource threads");
498 } else {
499 self.in_flight.borrow_mut().push_back(p.clone());
500 }
501
502 p
504 }
505
506 fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
508 let global = self.global();
510
511 let origin = global.origin();
513
514 let p = Promise::new(&global, can_gc);
516
517 if !origin.is_tuple() {
519 p.reject_error(Error::Security, can_gc);
520 return p;
521 }
522
523 let res = global
526 .resource_threads()
527 .send(CoreResourceMsg::DeleteCookieAsync(
528 self.store_id,
529 global.creation_url().clone(),
530 name.0,
531 ));
532 if res.is_err() {
533 error!("Failed to send cookiestore message to resource threads");
534 } else {
535 self.in_flight.borrow_mut().push_back(p.clone());
536 }
537
538 p
540 }
541
542 fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
544 let global = self.global();
546
547 let origin = global.origin();
549
550 let p = Promise::new(&global, can_gc);
552
553 if !origin.is_tuple() {
555 p.reject_error(Error::Security, can_gc);
556 return p;
557 }
558
559 let res = global
562 .resource_threads()
563 .send(CoreResourceMsg::DeleteCookieAsync(
564 self.store_id,
565 global.creation_url().clone(),
566 options.name.to_string(),
567 ));
568 if res.is_err() {
569 error!("Failed to send cookiestore message to resource threads");
570 } else {
571 self.in_flight.borrow_mut().push_back(p.clone());
572 }
573
574 p
576 }
577}
578
579impl Drop for CookieStore {
580 fn drop(&mut self) {
581 let res = self
582 .unregister_channel
583 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
584 if res.is_err() {
585 error!("Failed to send cookiestore message to resource threads");
586 }
587 }
588}