1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::id::CookieStoreId;
10use cookie::Expiration::DateTime;
11use cookie::{Cookie, SameSite};
12use dom_struct::dom_struct;
13use hyper_serde::Serde;
14use ipc_channel::ipc;
15use ipc_channel::router::ROUTER;
16use itertools::Itertools;
17use js::jsval::NullValue;
18use net_traits::CookieSource::NonHTTP;
19use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg, IpcSend};
20use script_bindings::script_runtime::CanGc;
21use servo_url::ServoUrl;
22
23use crate::dom::bindings::cell::DomRefCell;
24use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
25 CookieInit, CookieListItem, CookieSameSite, CookieStoreDeleteOptions, CookieStoreGetOptions,
26 CookieStoreMethods,
27};
28use crate::dom::bindings::error::Error;
29use crate::dom::bindings::num::Finite;
30use crate::dom::bindings::refcounted::Trusted;
31use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::bindings::str::USVString;
34use crate::dom::eventtarget::EventTarget;
35use crate::dom::globalscope::GlobalScope;
36use crate::dom::promise::Promise;
37use crate::dom::window::Window;
38use crate::task_source::SendableTaskSource;
39
40#[dom_struct]
45pub(crate) struct CookieStore {
46 eventtarget: EventTarget,
47 #[ignore_malloc_size_of = "Rc"]
48 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
49 #[no_trace]
51 store_id: CookieStoreId,
52}
53
54struct CookieListener {
55 task_source: SendableTaskSource,
57 context: Trusted<CookieStore>,
58}
59
60impl CookieListener {
61 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
62 let context = self.context.clone();
63 self.task_source.queue(task!(cookie_message: move || {
64 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
65 warn!("No promise exists for cookie store response");
66 return;
67 };
68 match message.data {
69 CookieData::Get(cookie) => {
70 if let Some(cookie) = cookie {
73 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
75 } else {
76 promise.resolve_native(&NullValue(), CanGc::note());
78 }
79 },
80 CookieData::GetAll(cookies) => {
81 promise.resolve_native(
83 &cookies
84 .into_iter()
85 .map(|cookie| cookie_to_list_item(cookie.0))
86 .collect_vec(),
87 CanGc::note());
88 },
89 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
90 promise.resolve_native(&(), CanGc::note());
91 }
92 }
93 }));
94 }
95}
96
97impl CookieStore {
98 fn new_inherited() -> CookieStore {
99 CookieStore {
100 eventtarget: EventTarget::new_inherited(),
101 in_flight: Default::default(),
102 store_id: CookieStoreId::new(),
103 }
104 }
105
106 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
107 let store = reflect_dom_object(Box::new(CookieStore::new_inherited()), global, can_gc);
108 store.setup_route();
109 store
110 }
111
112 fn setup_route(&self) {
113 let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
114
115 let context = Trusted::new(self);
116 let cs_listener = CookieListener {
117 task_source: self
118 .global()
119 .task_manager()
120 .dom_manipulation_task_source()
121 .to_sendable(),
122 context,
123 };
124
125 ROUTER.add_typed_route(
126 cookie_receiver,
127 Box::new(move |message| match message {
128 Ok(msg) => cs_listener.handle(msg),
129 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
130 }),
131 );
132
133 let res = self
134 .global()
135 .resource_threads()
136 .send(CoreResourceMsg::NewCookieListener(
137 self.store_id,
138 cookie_sender,
139 self.global().creation_url().clone(),
140 ));
141 if res.is_err() {
142 error!("Failed to send cookiestore message to resource threads");
143 }
144 }
145}
146
147fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
149 CookieListItem {
152 domain: cookie
154 .domain()
155 .map(|domain| Some(domain.to_string().into())),
156
157 expires: match cookie.expires() {
159 None | Some(cookie::Expiration::Session) => None,
160 Some(DateTime(time)) => Some(Some(Finite::wrap((time.unix_timestamp() * 1000) as f64))),
161 },
162
163 name: Some(cookie.name().to_string().into()),
165
166 partitioned: Some(false), path: cookie.path().map(|path| path.to_string().into()),
172
173 sameSite: match cookie.same_site() {
174 Some(SameSite::None) => Some(CookieSameSite::None),
175 Some(SameSite::Lax) => Some(CookieSameSite::Lax),
176 Some(SameSite::Strict) => Some(CookieSameSite::Strict),
177 None => None, },
179
180 secure: cookie.secure(),
182
183 value: Some(cookie.value().to_string().into()),
185 }
186}
187
188impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
189 fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
191 let global = self.global();
193
194 let origin = global.origin();
196
197 let p = Promise::new(&global, can_gc);
199
200 if !origin.is_tuple() {
202 p.reject_error(Error::Security, can_gc);
203 return p;
204 }
205
206 let creation_url = global.creation_url();
208
209 let res = self
211 .global()
212 .resource_threads()
213 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
214 self.store_id,
215 creation_url.clone(),
216 Some(name.to_string()),
217 ));
218 if res.is_err() {
219 error!("Failed to send cookiestore message to resource threads");
220 } else {
221 self.in_flight.borrow_mut().push_back(p.clone());
222 }
223
224 p
226 }
227
228 fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
230 let global = self.global();
232
233 let origin = global.origin();
235
236 let p = Promise::new(&global, can_gc);
238
239 if !origin.is_tuple() {
241 p.reject_error(Error::Security, can_gc);
242 return p;
243 }
244
245 let creation_url = global.creation_url();
247
248 if options.url.is_none() && options.name.is_none() {
251 p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
252 return p;
253 }
254
255 let mut final_url = creation_url.clone();
256
257 if let Some(get_url) = &options.url {
259 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
261
262 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
265 if parsed_url
266 .as_ref()
267 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
268 {
269 p.reject_error(
270 Error::Type("URL does not match context".to_string()),
271 can_gc,
272 );
273 return p;
274 }
275 }
276
277 if parsed_url
280 .as_ref()
281 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
282 {
283 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
284 return p;
285 }
286
287 if let Ok(url) = parsed_url {
289 final_url = url;
290 }
291 }
292
293 let res = self
295 .global()
296 .resource_threads()
297 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
298 self.store_id,
299 final_url.clone(),
300 options.name.clone().map(|val| val.0),
301 ));
302 if res.is_err() {
303 error!("Failed to send cookiestore message to resource threads");
304 } else {
305 self.in_flight.borrow_mut().push_back(p.clone());
306 }
307
308 p
309 }
310
311 fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
313 let global = self.global();
315
316 let origin = global.origin();
318
319 let p = Promise::new(&global, can_gc);
321
322 if !origin.is_tuple() {
324 p.reject_error(Error::Security, can_gc);
325 return p;
326 }
327 let creation_url = global.creation_url();
329
330 let res =
332 self.global()
333 .resource_threads()
334 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
335 self.store_id,
336 creation_url.clone(),
337 Some(name.to_string()),
338 ));
339 if res.is_err() {
340 error!("Failed to send cookiestore message to resource threads");
341 } else {
342 self.in_flight.borrow_mut().push_back(p.clone());
343 }
344
345 p
347 }
348
349 fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
351 let global = self.global();
353
354 let origin = global.origin();
356
357 let p = Promise::new(&global, can_gc);
359
360 if !origin.is_tuple() {
362 p.reject_error(Error::Security, can_gc);
363 return p;
364 }
365
366 let creation_url = global.creation_url();
368
369 let mut final_url = creation_url.clone();
370
371 if let Some(get_url) = &options.url {
373 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
375
376 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
379 if parsed_url
380 .as_ref()
381 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
382 {
383 p.reject_error(
384 Error::Type("URL does not match context".to_string()),
385 can_gc,
386 );
387 return p;
388 }
389 }
390
391 if parsed_url
394 .as_ref()
395 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
396 {
397 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
398 return p;
399 }
400
401 if let Ok(url) = parsed_url {
403 final_url = url;
404 }
405 }
406
407 let res =
409 self.global()
410 .resource_threads()
411 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
412 self.store_id,
413 final_url.clone(),
414 options.name.clone().map(|val| val.0),
415 ));
416 if res.is_err() {
417 error!("Failed to send cookiestore message to resource threads");
418 } else {
419 self.in_flight.borrow_mut().push_back(p.clone());
420 }
421
422 p
424 }
425
426 fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
428 let global = self.global();
430
431 let origin = global.origin();
433
434 let p = Promise::new(&global, can_gc);
436
437 if !origin.is_tuple() {
439 p.reject_error(Error::Security, can_gc);
440 return p;
441 }
442
443 let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
449 .path("/")
450 .secure(true)
451 .same_site(SameSite::Strict)
452 .partitioned(false);
453 let res = self
458 .global()
459 .resource_threads()
460 .send(CoreResourceMsg::SetCookieForUrlAsync(
461 self.store_id,
462 self.global().creation_url().clone(),
463 Serde(cookie.build()),
464 NonHTTP,
465 ));
466 if res.is_err() {
467 error!("Failed to send cookiestore message to resource threads");
468 } else {
469 self.in_flight.borrow_mut().push_back(p.clone());
470 }
471
472 p
474 }
475
476 fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
478 let global = self.global();
480
481 let origin = global.origin();
483
484 let p = Promise::new(&global, can_gc);
486
487 if !origin.is_tuple() {
489 p.reject_error(Error::Security, can_gc);
490 return p;
491 }
492
493 let creation_url = global.creation_url();
495
496 let cookie = Cookie::build((
499 Cow::Owned(options.name.to_string()),
500 Cow::Owned(options.value.to_string()),
501 ));
502 let res = self
507 .global()
508 .resource_threads()
509 .send(CoreResourceMsg::SetCookieForUrlAsync(
510 self.store_id,
511 creation_url.clone(),
512 Serde(cookie.build()),
513 NonHTTP,
514 ));
515 if res.is_err() {
516 error!("Failed to send cookiestore message to resource threads");
517 } else {
518 self.in_flight.borrow_mut().push_back(p.clone());
519 }
520
521 p
523 }
524
525 fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
527 let global = self.global();
529
530 let origin = global.origin();
532
533 let p = Promise::new(&global, can_gc);
535
536 if !origin.is_tuple() {
538 p.reject_error(Error::Security, can_gc);
539 return p;
540 }
541
542 let res = global
545 .resource_threads()
546 .send(CoreResourceMsg::DeleteCookieAsync(
547 self.store_id,
548 global.creation_url().clone(),
549 name.0,
550 ));
551 if res.is_err() {
552 error!("Failed to send cookiestore message to resource threads");
553 } else {
554 self.in_flight.borrow_mut().push_back(p.clone());
555 }
556
557 p
559 }
560
561 fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
563 let global = self.global();
565
566 let origin = global.origin();
568
569 let p = Promise::new(&global, can_gc);
571
572 if !origin.is_tuple() {
574 p.reject_error(Error::Security, can_gc);
575 return p;
576 }
577
578 let res = global
581 .resource_threads()
582 .send(CoreResourceMsg::DeleteCookieAsync(
583 self.store_id,
584 global.creation_url().clone(),
585 options.name.to_string(),
586 ));
587 if res.is_err() {
588 error!("Failed to send cookiestore message to resource threads");
589 } else {
590 self.in_flight.borrow_mut().push_back(p.clone());
591 }
592
593 p
595 }
596}
597
598impl Drop for CookieStore {
599 fn drop(&mut self) {
600 let res = self
601 .global()
602 .resource_threads()
603 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
604 if res.is_err() {
605 error!("Failed to send cookiestore message to resource threads");
606 }
607 }
608}