1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::id::CookieStoreId;
10use cookie::{Cookie, SameSite};
11use dom_struct::dom_struct;
12use hyper_serde::Serde;
13use ipc_channel::ipc;
14use ipc_channel::ipc::IpcSender;
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, 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#[dom_struct]
43pub(crate) struct CookieStore {
44 eventtarget: EventTarget,
45 #[ignore_malloc_size_of = "Rc"]
46 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
47 #[no_trace]
49 store_id: CookieStoreId,
50 #[ignore_malloc_size_of = "Channels are hard"]
51 #[no_trace]
52 unregister_channel: IpcSender<CoreResourceMsg>,
53}
54
55struct CookieListener {
56 task_source: SendableTaskSource,
58 context: Trusted<CookieStore>,
59}
60
61impl CookieListener {
62 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
63 let context = self.context.clone();
64 self.task_source.queue(task!(cookie_message: move || {
65 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
66 warn!("No promise exists for cookie store response");
67 return;
68 };
69 match message.data {
70 CookieData::Get(cookie) => {
71 if let Some(cookie) = cookie {
74 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
76 } else {
77 promise.resolve_native(&NullValue(), CanGc::note());
79 }
80 },
81 CookieData::GetAll(cookies) => {
82 promise.resolve_native(
84 &cookies
85 .into_iter()
86 .map(|cookie| cookie_to_list_item(cookie.0))
87 .collect_vec(),
88 CanGc::note());
89 },
90 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
91 promise.resolve_native(&(), CanGc::note());
92 }
93 }
94 }));
95 }
96}
97
98impl CookieStore {
99 fn new_inherited(unregister_channel: IpcSender<CoreResourceMsg>) -> CookieStore {
100 CookieStore {
101 eventtarget: EventTarget::new_inherited(),
102 in_flight: Default::default(),
103 store_id: CookieStoreId::new(),
104 unregister_channel,
105 }
106 }
107
108 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
109 let store = reflect_dom_object(
110 Box::new(CookieStore::new_inherited(
111 global.resource_threads().core_thread.clone(),
112 )),
113 global,
114 can_gc,
115 );
116 store.setup_route();
117 store
118 }
119
120 fn setup_route(&self) {
121 let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
122
123 let context = Trusted::new(self);
124 let cs_listener = CookieListener {
125 task_source: self
126 .global()
127 .task_manager()
128 .dom_manipulation_task_source()
129 .to_sendable(),
130 context,
131 };
132
133 ROUTER.add_typed_route(
134 cookie_receiver,
135 Box::new(move |message| match message {
136 Ok(msg) => cs_listener.handle(msg),
137 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
138 }),
139 );
140
141 let res = self
142 .global()
143 .resource_threads()
144 .send(CoreResourceMsg::NewCookieListener(
145 self.store_id,
146 cookie_sender,
147 self.global().creation_url().clone(),
148 ));
149 if res.is_err() {
150 error!("Failed to send cookiestore message to resource threads");
151 }
152 }
153}
154
155fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
157 CookieListItem {
160 name: Some(cookie.name().to_string().into()),
162
163 value: Some(cookie.value().to_string().into()),
165 }
166}
167
168impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
169 fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
171 let global = self.global();
173
174 let origin = global.origin();
176
177 let p = Promise::new(&global, can_gc);
179
180 if !origin.is_tuple() {
182 p.reject_error(Error::Security, can_gc);
183 return p;
184 }
185
186 let creation_url = global.creation_url();
188
189 let res = self
191 .global()
192 .resource_threads()
193 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
194 self.store_id,
195 creation_url.clone(),
196 Some(name.to_string()),
197 ));
198 if res.is_err() {
199 error!("Failed to send cookiestore message to resource threads");
200 } else {
201 self.in_flight.borrow_mut().push_back(p.clone());
202 }
203
204 p
206 }
207
208 fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
210 let global = self.global();
212
213 let origin = global.origin();
215
216 let p = Promise::new(&global, can_gc);
218
219 if !origin.is_tuple() {
221 p.reject_error(Error::Security, can_gc);
222 return p;
223 }
224
225 let creation_url = global.creation_url();
227
228 if options.url.is_none() && options.name.is_none() {
231 p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
232 return p;
233 }
234
235 let mut final_url = creation_url.clone();
236
237 if let Some(get_url) = &options.url {
239 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
241
242 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
245 if parsed_url
246 .as_ref()
247 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
248 {
249 p.reject_error(
250 Error::Type("URL does not match context".to_string()),
251 can_gc,
252 );
253 return p;
254 }
255 }
256
257 if parsed_url
260 .as_ref()
261 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
262 {
263 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
264 return p;
265 }
266
267 if let Ok(url) = parsed_url {
269 final_url = url;
270 }
271 }
272
273 let res = self
275 .global()
276 .resource_threads()
277 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
278 self.store_id,
279 final_url.clone(),
280 options.name.clone().map(|val| val.0),
281 ));
282 if res.is_err() {
283 error!("Failed to send cookiestore message to resource threads");
284 } else {
285 self.in_flight.borrow_mut().push_back(p.clone());
286 }
287
288 p
289 }
290
291 fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
293 let global = self.global();
295
296 let origin = global.origin();
298
299 let p = Promise::new(&global, can_gc);
301
302 if !origin.is_tuple() {
304 p.reject_error(Error::Security, can_gc);
305 return p;
306 }
307 let creation_url = global.creation_url();
309
310 let res =
312 self.global()
313 .resource_threads()
314 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
315 self.store_id,
316 creation_url.clone(),
317 Some(name.to_string()),
318 ));
319 if res.is_err() {
320 error!("Failed to send cookiestore message to resource threads");
321 } else {
322 self.in_flight.borrow_mut().push_back(p.clone());
323 }
324
325 p
327 }
328
329 fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
331 let global = self.global();
333
334 let origin = global.origin();
336
337 let p = Promise::new(&global, can_gc);
339
340 if !origin.is_tuple() {
342 p.reject_error(Error::Security, can_gc);
343 return p;
344 }
345
346 let creation_url = global.creation_url();
348
349 let mut final_url = creation_url.clone();
350
351 if let Some(get_url) = &options.url {
353 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
355
356 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
359 if parsed_url
360 .as_ref()
361 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
362 {
363 p.reject_error(
364 Error::Type("URL does not match context".to_string()),
365 can_gc,
366 );
367 return p;
368 }
369 }
370
371 if parsed_url
374 .as_ref()
375 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
376 {
377 p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
378 return p;
379 }
380
381 if let Ok(url) = parsed_url {
383 final_url = url;
384 }
385 }
386
387 let res =
389 self.global()
390 .resource_threads()
391 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
392 self.store_id,
393 final_url.clone(),
394 options.name.clone().map(|val| val.0),
395 ));
396 if res.is_err() {
397 error!("Failed to send cookiestore message to resource threads");
398 } else {
399 self.in_flight.borrow_mut().push_back(p.clone());
400 }
401
402 p
404 }
405
406 fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
408 let global = self.global();
410
411 let origin = global.origin();
413
414 let p = Promise::new(&global, can_gc);
416
417 if !origin.is_tuple() {
419 p.reject_error(Error::Security, can_gc);
420 return p;
421 }
422
423 let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
429 .path("/")
430 .secure(true)
431 .same_site(SameSite::Strict)
432 .partitioned(false);
433 let res = self
438 .global()
439 .resource_threads()
440 .send(CoreResourceMsg::SetCookieForUrlAsync(
441 self.store_id,
442 self.global().creation_url().clone(),
443 Serde(cookie.build()),
444 NonHTTP,
445 ));
446 if res.is_err() {
447 error!("Failed to send cookiestore message to resource threads");
448 } else {
449 self.in_flight.borrow_mut().push_back(p.clone());
450 }
451
452 p
454 }
455
456 fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
458 let global = self.global();
460
461 let origin = global.origin();
463
464 let p = Promise::new(&global, can_gc);
466
467 if !origin.is_tuple() {
469 p.reject_error(Error::Security, can_gc);
470 return p;
471 }
472
473 let creation_url = global.creation_url();
475
476 let cookie = Cookie::build((
479 Cow::Owned(options.name.to_string()),
480 Cow::Owned(options.value.to_string()),
481 ));
482 let res = self
487 .global()
488 .resource_threads()
489 .send(CoreResourceMsg::SetCookieForUrlAsync(
490 self.store_id,
491 creation_url.clone(),
492 Serde(cookie.build()),
493 NonHTTP,
494 ));
495 if res.is_err() {
496 error!("Failed to send cookiestore message to resource threads");
497 } else {
498 self.in_flight.borrow_mut().push_back(p.clone());
499 }
500
501 p
503 }
504
505 fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
507 let global = self.global();
509
510 let origin = global.origin();
512
513 let p = Promise::new(&global, can_gc);
515
516 if !origin.is_tuple() {
518 p.reject_error(Error::Security, can_gc);
519 return p;
520 }
521
522 let res = global
525 .resource_threads()
526 .send(CoreResourceMsg::DeleteCookieAsync(
527 self.store_id,
528 global.creation_url().clone(),
529 name.0,
530 ));
531 if res.is_err() {
532 error!("Failed to send cookiestore message to resource threads");
533 } else {
534 self.in_flight.borrow_mut().push_back(p.clone());
535 }
536
537 p
539 }
540
541 fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
543 let global = self.global();
545
546 let origin = global.origin();
548
549 let p = Promise::new(&global, can_gc);
551
552 if !origin.is_tuple() {
554 p.reject_error(Error::Security, can_gc);
555 return p;
556 }
557
558 let res = global
561 .resource_threads()
562 .send(CoreResourceMsg::DeleteCookieAsync(
563 self.store_id,
564 global.creation_url().clone(),
565 options.name.to_string(),
566 ));
567 if res.is_err() {
568 error!("Failed to send cookiestore message to resource threads");
569 } else {
570 self.in_flight.borrow_mut().push_back(p.clone());
571 }
572
573 p
575 }
576}
577
578impl Drop for CookieStore {
579 fn drop(&mut self) {
580 let res = self
581 .unregister_channel
582 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
583 if res.is_err() {
584 error!("Failed to send cookiestore message to resource threads");
585 }
586 }
587}