1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::generic_channel::{GenericSend, GenericSender};
10use base::id::CookieStoreId;
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};
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, CookieStoreDeleteOptions, CookieStoreGetOptions, CookieStoreMethods,
26};
27use crate::dom::bindings::error::Error;
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
30use crate::dom::bindings::root::DomRoot;
31use crate::dom::bindings::str::USVString;
32use crate::dom::eventtarget::EventTarget;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::promise::Promise;
35use crate::dom::window::Window;
36use crate::task_source::SendableTaskSource;
37
38#[derive(JSTraceable, MallocSizeOf)]
39struct DroppableCookieStore {
40 #[no_trace]
42 store_id: CookieStoreId,
43 #[ignore_malloc_size_of = "Channels are hard"]
44 #[no_trace]
45 unregister_channel: GenericSender<CoreResourceMsg>,
46}
47
48impl Drop for DroppableCookieStore {
49 fn drop(&mut self) {
50 let res = self
51 .unregister_channel
52 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
53 if res.is_err() {
54 error!("Failed to send cookiestore message to resource threads");
55 }
56 }
57}
58
59#[dom_struct]
64pub(crate) struct CookieStore {
65 eventtarget: EventTarget,
66 #[conditional_malloc_size_of]
67 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
68 droppable: DroppableCookieStore,
69}
70
71struct CookieListener {
72 task_source: SendableTaskSource,
74 context: Trusted<CookieStore>,
75}
76
77impl CookieListener {
78 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
79 let context = self.context.clone();
80 self.task_source.queue(task!(cookie_message: move || {
81 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
82 warn!("No promise exists for cookie store response");
83 return;
84 };
85 match message.data {
86 CookieData::Get(cookie) => {
87 if let Some(cookie) = cookie {
90 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
92 } else {
93 promise.resolve_native(&NullValue(), CanGc::note());
95 }
96 },
97 CookieData::GetAll(cookies) => {
98 promise.resolve_native(
100 &cookies
101 .into_iter()
102 .map(|cookie| cookie_to_list_item(cookie.0))
103 .collect_vec(),
104 CanGc::note());
105 },
106 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
107 promise.resolve_native(&(), CanGc::note());
108 }
109 }
110 }));
111 }
112}
113
114impl CookieStore {
115 fn new_inherited(unregister_channel: GenericSender<CoreResourceMsg>) -> CookieStore {
116 CookieStore {
117 eventtarget: EventTarget::new_inherited(),
118 in_flight: Default::default(),
119 droppable: DroppableCookieStore {
120 store_id: CookieStoreId::new(),
121 unregister_channel,
122 },
123 }
124 }
125
126 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
127 let store = reflect_dom_object(
128 Box::new(CookieStore::new_inherited(
129 global.resource_threads().core_thread.clone(),
130 )),
131 global,
132 can_gc,
133 );
134 store.setup_route();
135 store
136 }
137
138 fn setup_route(&self) {
139 let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
140
141 let context = Trusted::new(self);
142 let cs_listener = CookieListener {
143 task_source: self
144 .global()
145 .task_manager()
146 .dom_manipulation_task_source()
147 .to_sendable(),
148 context,
149 };
150
151 ROUTER.add_typed_route(
152 cookie_receiver,
153 Box::new(move |message| match message {
154 Ok(msg) => cs_listener.handle(msg),
155 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
156 }),
157 );
158
159 let res = self
160 .global()
161 .resource_threads()
162 .send(CoreResourceMsg::NewCookieListener(
163 self.droppable.store_id,
164 cookie_sender,
165 self.global().creation_url().clone(),
166 ));
167 if res.is_err() {
168 error!("Failed to send cookiestore message to resource threads");
169 }
170 }
171}
172
173fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
175 CookieListItem {
178 name: Some(cookie.name().to_string().into()),
180
181 value: Some(cookie.value().to_string().into()),
183 }
184}
185
186impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
187 fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
189 let global = self.global();
191
192 let origin = global.origin();
194
195 let p = Promise::new(&global, can_gc);
197
198 if !origin.is_tuple() {
200 p.reject_error(Error::Security(None), can_gc);
201 return p;
202 }
203
204 let creation_url = global.creation_url();
206
207 let res = self
209 .global()
210 .resource_threads()
211 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
212 self.droppable.store_id,
213 creation_url.clone(),
214 Some(name.into()),
215 ));
216 if res.is_err() {
217 error!("Failed to send cookiestore message to resource threads");
218 } else {
219 self.in_flight.borrow_mut().push_back(p.clone());
220 }
221
222 p
224 }
225
226 fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
228 let global = self.global();
230
231 let origin = global.origin();
233
234 let p = Promise::new(&global, can_gc);
236
237 if !origin.is_tuple() {
239 p.reject_error(Error::Security(None), can_gc);
240 return p;
241 }
242
243 let creation_url = global.creation_url();
245
246 if options.url.is_none() && options.name.is_none() {
249 p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
250 return p;
251 }
252
253 let mut final_url = creation_url.clone();
254
255 if let Some(get_url) = &options.url {
257 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
259
260 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
263 if parsed_url
264 .as_ref()
265 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
266 {
267 p.reject_error(
268 Error::Type("URL does not match context".to_string()),
269 can_gc,
270 );
271 return p;
272 }
273 }
274
275 if parsed_url
278 .as_ref()
279 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
280 {
281 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
282 return p;
283 }
284
285 if let Ok(url) = parsed_url {
287 final_url = url;
288 }
289 }
290
291 let res = self
293 .global()
294 .resource_threads()
295 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
296 self.droppable.store_id,
297 final_url.clone(),
298 options.name.clone().map(|val| val.0),
299 ));
300 if res.is_err() {
301 error!("Failed to send cookiestore message to resource threads");
302 } else {
303 self.in_flight.borrow_mut().push_back(p.clone());
304 }
305
306 p
307 }
308
309 fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
311 let global = self.global();
313
314 let origin = global.origin();
316
317 let p = Promise::new(&global, can_gc);
319
320 if !origin.is_tuple() {
322 p.reject_error(Error::Security(None), can_gc);
323 return p;
324 }
325 let creation_url = global.creation_url();
327
328 let res =
330 self.global()
331 .resource_threads()
332 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
333 self.droppable.store_id,
334 creation_url.clone(),
335 Some(name.to_string()),
336 ));
337 if res.is_err() {
338 error!("Failed to send cookiestore message to resource threads");
339 } else {
340 self.in_flight.borrow_mut().push_back(p.clone());
341 }
342
343 p
345 }
346
347 fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
349 let global = self.global();
351
352 let origin = global.origin();
354
355 let p = Promise::new(&global, can_gc);
357
358 if !origin.is_tuple() {
360 p.reject_error(Error::Security(None), can_gc);
361 return p;
362 }
363
364 let creation_url = global.creation_url();
366
367 let mut final_url = creation_url.clone();
368
369 if let Some(get_url) = &options.url {
371 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
373
374 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
377 if parsed_url
378 .as_ref()
379 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
380 {
381 p.reject_error(
382 Error::Type("URL does not match context".to_string()),
383 can_gc,
384 );
385 return p;
386 }
387 }
388
389 if parsed_url
392 .as_ref()
393 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
394 {
395 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
396 return p;
397 }
398
399 if let Ok(url) = parsed_url {
401 final_url = url;
402 }
403 }
404
405 let res =
407 self.global()
408 .resource_threads()
409 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
410 self.droppable.store_id,
411 final_url.clone(),
412 options.name.clone().map(|val| val.0),
413 ));
414 if res.is_err() {
415 error!("Failed to send cookiestore message to resource threads");
416 } else {
417 self.in_flight.borrow_mut().push_back(p.clone());
418 }
419
420 p
422 }
423
424 fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
426 let global = self.global();
428
429 let origin = global.origin();
431
432 let p = Promise::new(&global, can_gc);
434
435 if !origin.is_tuple() {
437 p.reject_error(Error::Security(None), can_gc);
438 return p;
439 }
440
441 let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
447 .path("/")
448 .secure(true)
449 .same_site(SameSite::Strict)
450 .partitioned(false);
451 let res = self
456 .global()
457 .resource_threads()
458 .send(CoreResourceMsg::SetCookieForUrlAsync(
459 self.droppable.store_id,
460 self.global().creation_url().clone(),
461 Serde(cookie.build()),
462 NonHTTP,
463 ));
464 if res.is_err() {
465 error!("Failed to send cookiestore message to resource threads");
466 } else {
467 self.in_flight.borrow_mut().push_back(p.clone());
468 }
469
470 p
472 }
473
474 fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
476 let global = self.global();
478
479 let origin = global.origin();
481
482 let p = Promise::new(&global, can_gc);
484
485 if !origin.is_tuple() {
487 p.reject_error(Error::Security(None), can_gc);
488 return p;
489 }
490
491 let creation_url = global.creation_url();
493
494 let cookie = Cookie::build((
497 Cow::Owned(options.name.to_string()),
498 Cow::Owned(options.value.to_string()),
499 ));
500 let res = self
505 .global()
506 .resource_threads()
507 .send(CoreResourceMsg::SetCookieForUrlAsync(
508 self.droppable.store_id,
509 creation_url.clone(),
510 Serde(cookie.build()),
511 NonHTTP,
512 ));
513 if res.is_err() {
514 error!("Failed to send cookiestore message to resource threads");
515 } else {
516 self.in_flight.borrow_mut().push_back(p.clone());
517 }
518
519 p
521 }
522
523 fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
525 let global = self.global();
527
528 let origin = global.origin();
530
531 let p = Promise::new(&global, can_gc);
533
534 if !origin.is_tuple() {
536 p.reject_error(Error::Security(None), can_gc);
537 return p;
538 }
539
540 let res = global
543 .resource_threads()
544 .send(CoreResourceMsg::DeleteCookieAsync(
545 self.droppable.store_id,
546 global.creation_url().clone(),
547 name.0,
548 ));
549 if res.is_err() {
550 error!("Failed to send cookiestore message to resource threads");
551 } else {
552 self.in_flight.borrow_mut().push_back(p.clone());
553 }
554
555 p
557 }
558
559 fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
561 let global = self.global();
563
564 let origin = global.origin();
566
567 let p = Promise::new(&global, can_gc);
569
570 if !origin.is_tuple() {
572 p.reject_error(Error::Security(None), can_gc);
573 return p;
574 }
575
576 let res = global
579 .resource_threads()
580 .send(CoreResourceMsg::DeleteCookieAsync(
581 self.droppable.store_id,
582 global.creation_url().clone(),
583 options.name.to_string(),
584 ));
585 if res.is_err() {
586 error!("Failed to send cookiestore message to resource threads");
587 } else {
588 self.in_flight.borrow_mut().push_back(p.clone());
589 }
590
591 p
593 }
594}