1use std::cell::{Cell, RefCell};
24use std::collections::HashMap;
25use std::path::PathBuf;
26use std::rc::Rc;
27use std::time::Duration;
28
29use glib::prelude::*;
30use webkit6::gtk;
31use webkit6::prelude::*;
32use webkit6::{LoadEvent, UserContentInjectedFrames, UserScript, UserScriptInjectionTime, WebView};
33
34use vs_protocol::{Ref, Tree};
35
36use crate::engine::{
37 ActTarget, Action, AuthBlob, CaptureScope, CursorOp, Engine, EngineCapabilities, EngineError,
38 EngineResult, InputMode, LayoutBox, PageHandle, Viewport, WaitCondition,
39};
40
41struct WpePage {
46 web_view: WebView,
47 window: gtk::Window,
52 inspector: super::inspector_bridge::InspectorSlots,
53 inspector_installed: bool,
57 cookie_baseline: std::cell::RefCell<Option<Vec<super::auth::CookieData>>>,
58 cookie_next_seq: std::cell::RefCell<u64>,
59 last_mouse: Cell<vs_humanize::Point>,
63}
64
65pub struct WpeBackend {
72 pages: HashMap<PageHandle, WpePage>,
73 next_handle: u64,
74 captures_dir: Option<PathBuf>,
75}
76
77impl Default for WpeBackend {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl WpeBackend {
84 #[must_use]
89 pub fn new() -> Self {
90 Self {
91 pages: HashMap::new(),
92 next_handle: 1,
93 captures_dir: None,
94 }
95 }
96
97 #[must_use]
100 pub fn with_capture_dir(mut self, dir: impl Into<PathBuf>) -> Self {
101 self.captures_dir = Some(dir.into());
102 self
103 }
104
105 fn alloc_handle(&mut self) -> PageHandle {
106 let h = PageHandle(self.next_handle);
107 self.next_handle += 1;
108 h
109 }
110
111 fn page_mut(&mut self, h: PageHandle) -> EngineResult<&mut WpePage> {
112 self.pages.get_mut(&h).ok_or(EngineError::NotFound {
113 kind: "page",
114 id: h.0.to_string(),
115 })
116 }
117}
118
119fn run_loop_until<F: FnMut() -> bool>(mut done: F, budget: Duration) -> bool {
124 let main_ctx = glib::MainContext::default();
125 let deadline = std::time::Instant::now() + budget;
126 while std::time::Instant::now() < deadline {
127 if done() {
128 return true;
129 }
130 if !main_ctx.iteration(false) {
132 std::thread::sleep(Duration::from_millis(10));
133 }
134 }
135 done()
136}
137
138use super::common::{parse_snapshot, SNAPSHOT_DOM_WALKER_JS as SNAPSHOT_JS};
143
144impl Engine for WpeBackend {
149 fn open(&mut self, url: &str) -> EngineResult<PageHandle> {
150 let web_view = WebView::new();
151
152 let window = gtk::Window::new();
162 window.set_decorated(false);
163 window.set_default_size(1280, 800);
164 window.set_child(Some(&web_view));
165 window.present();
166
167 if let Some(settings) = WebViewExt::settings(&web_view) {
171 settings.set_user_agent(Some(crate::engine::DEFAULT_USER_AGENT));
172 }
173
174 let inspector =
178 super::inspector_bridge::InspectorSlots::new(crate::inspector::DEFAULT_BUFFER_CAPACITY);
179 let inspector_installed = install_inspector(&web_view, &inspector);
180 web_view.load_uri(url);
181
182 let slot: Rc<RefCell<Option<Result<(), String>>>> = Rc::new(RefCell::new(None));
184 let slot_for_signal = slot.clone();
185 let signal_id = web_view.connect_load_changed(move |_view, event| {
186 if event == LoadEvent::Finished {
187 *slot_for_signal.borrow_mut() = Some(Ok(()));
188 }
189 });
190 let slot_fail = slot.clone();
191 let fail_id = web_view.connect_load_failed(move |_view, _event, _uri, err| {
192 *slot_fail.borrow_mut() = Some(Err(err.message().to_string()));
193 true
195 });
196
197 let slot_check = slot.clone();
198 let ok = run_loop_until(
199 move || slot_check.borrow().is_some(),
200 Duration::from_secs(15),
201 );
202
203 web_view.disconnect(signal_id);
205 web_view.disconnect(fail_id);
206
207 if !ok {
208 return Err(EngineError::Timeout {
209 budget: Duration::from_secs(15),
210 primitive: "open",
211 });
212 }
213 match slot.borrow_mut().take() {
214 Some(Ok(())) => {}
215 Some(Err(msg)) => return Err(EngineError::Other(format!("navigation failed: {msg}"))),
216 None => unreachable!(),
217 }
218
219 let handle = self.alloc_handle();
220 self.pages.insert(
221 handle,
222 WpePage {
223 web_view,
224 window,
225 inspector,
226 inspector_installed,
227 cookie_baseline: std::cell::RefCell::new(None),
228 cookie_next_seq: std::cell::RefCell::new(0),
229 last_mouse: Cell::new(vs_humanize::Point { x: 0.0, y: 0.0 }),
230 },
231 );
232 Ok(handle)
233 }
234
235 fn close(&mut self, page: PageHandle) -> EngineResult<()> {
236 if let Some(p) = self.pages.remove(&page) {
237 p.window.close();
242 }
243 Ok(())
244 }
245
246 fn snapshot(&mut self, page: PageHandle) -> EngineResult<Tree> {
247 let p = self.page_mut(page)?;
248 let json = eval_js_string(&p.web_view, SNAPSHOT_JS, Duration::from_secs(5))?;
249 parse_snapshot(&json).map_err(EngineError::Other)
250 }
251
252 fn act(&mut self, page: PageHandle, target: ActTarget, action: Action) -> EngineResult<()> {
253 let p = self.page_mut(page)?;
254 let web_view = p.web_view.clone();
255 super::common::run_act(
256 move |js, budget| eval_js_string(&web_view, js, budget),
257 &target,
258 &action,
259 )
260 }
261
262 fn wait(
263 &mut self,
264 page: PageHandle,
265 cond: WaitCondition,
266 budget: Duration,
267 ) -> EngineResult<()> {
268 let p = self.page_mut(page)?;
269 let web_view = p.web_view.clone();
270 super::common::run_wait(
271 |js, budget| eval_js_string(&web_view, js, budget),
272 &cond,
273 budget,
274 || {
275 let _ = run_loop_until(|| false, Duration::from_millis(50));
276 },
277 )
278 }
279 fn layout(&mut self, page: PageHandle, refs: &[Ref]) -> EngineResult<Vec<LayoutBox>> {
280 let p = self.page_mut(page)?;
281 let web_view = p.web_view.clone();
282 super::common::run_layout(
283 move |js, budget| eval_js_string(&web_view, js, budget),
284 refs,
285 )
286 }
287 fn set_viewport(&mut self, page: PageHandle, viewport: Viewport) -> EngineResult<()> {
288 let p = self.page_mut(page)?;
289 let w = i32::try_from(viewport.width).unwrap_or(i32::MAX);
290 let h = i32::try_from(viewport.height).unwrap_or(i32::MAX);
291 p.window.set_default_size(w, h);
298 p.web_view.set_size_request(w, h);
299 Ok(())
300 }
301
302 fn capture(&mut self, page: PageHandle, _scope: CaptureScope) -> EngineResult<PathBuf> {
303 let captures_dir = self.captures_dir.clone().unwrap_or_else(std::env::temp_dir);
304 let _ = std::fs::create_dir_all(&captures_dir);
305 let path = captures_dir.join(format!("capture-{}.png", page.0));
306 let p = self.page_mut(page)?;
307 let web_view = p.web_view.clone();
308 let out_path = path.clone();
309
310 let slot: Rc<RefCell<Option<Result<(), String>>>> = Rc::new(RefCell::new(None));
311 let slot_for_cb = slot.clone();
312 web_view.snapshot(
313 webkit6::SnapshotRegion::FullDocument,
314 webkit6::SnapshotOptions::NONE,
315 None::<&webkit6::gio::Cancellable>,
316 move |result| {
317 *slot_for_cb.borrow_mut() = Some(match result {
318 Ok(texture) => texture.save_to_png(&out_path).map_err(|e| e.to_string()),
319 Err(e) => Err(e.to_string()),
320 });
321 },
322 );
323
324 let slot_check = slot.clone();
325 let ok = run_loop_until(
326 move || slot_check.borrow().is_some(),
327 Duration::from_secs(10),
328 );
329 if !ok {
330 return Err(EngineError::Timeout {
331 budget: Duration::from_secs(10),
332 primitive: "capture",
333 });
334 }
335 let result = slot.borrow_mut().take();
336 match result {
337 Some(Ok(())) => Ok(path),
338 Some(Err(msg)) => Err(EngineError::Other(format!("capture: {msg}"))),
339 None => unreachable!(),
340 }
341 }
342
343 fn save_auth(&mut self, page: PageHandle) -> EngineResult<AuthBlob> {
344 let p = self.page_mut(page)?;
345 let web_view = p.web_view.clone();
346 let cookies = wpe_cookies::get_all_cookies(&web_view)?;
349 let storage = super::common::run_save_storage_only(move |js, budget| {
350 eval_js_string(&web_view, js, budget)
351 })?;
352 let blob = super::auth::AuthBlobV2 {
353 version: 2,
354 url: storage.url,
355 origin: storage.origin,
356 cookies,
357 local_storage: storage.local_storage,
358 session_storage: storage.session_storage,
359 };
360 super::auth::encode(&blob)
361 }
362
363 fn load_auth(&mut self, page: PageHandle, blob: &AuthBlob) -> EngineResult<()> {
364 let p = self.page_mut(page)?;
365 let web_view = p.web_view.clone();
366 let parsed = super::auth::decode(blob)?;
367 wpe_cookies::set_cookies(&web_view, &parsed.cookies)?;
368 super::common::run_load_storage_only(
369 move |js, budget| eval_js_string(&web_view, js, budget),
370 &parsed.local_storage,
371 &parsed.session_storage,
372 )
373 }
374
375 fn console_entries(
376 &mut self,
377 page: PageHandle,
378 ) -> EngineResult<Vec<crate::inspector::ConsoleEntry>> {
379 let p = self.page_mut(page)?;
380 Ok(p.inspector.console.borrow().snapshot())
381 }
382
383 fn network_entries(
384 &mut self,
385 page: PageHandle,
386 ) -> EngineResult<Vec<crate::inspector::NetworkEntry>> {
387 let p = self.page_mut(page)?;
388 Ok(p.inspector.network.borrow().snapshot())
389 }
390
391 fn request_detail(
392 &mut self,
393 page: PageHandle,
394 seq: u64,
395 ) -> EngineResult<Option<crate::inspector::RequestDetail>> {
396 let p = self.page_mut(page)?;
397 Ok(p.inspector.details.borrow().get(seq).cloned())
398 }
399
400 fn eval_js(
401 &mut self,
402 page: PageHandle,
403 expr: &str,
404 ) -> EngineResult<crate::inspector::EvalResult> {
405 let p = self.page_mut(page)?;
406 let web_view = p.web_view.clone();
407 super::common::run_eval(
408 move |js, budget| eval_js_string(&web_view, js, budget),
409 expr,
410 )
411 }
412
413 fn storage(
414 &mut self,
415 page: PageHandle,
416 scope: crate::inspector::StorageScope,
417 ) -> EngineResult<Vec<crate::inspector::StorageEntry>> {
418 let p = self.page_mut(page)?;
419 let web_view = p.web_view.clone();
420 if matches!(scope, crate::inspector::StorageScope::Cookies) {
421 let cookies = wpe_cookies::get_all_cookies(&web_view)?;
422 return Ok(cookies
423 .iter()
424 .map(super::common::cookie_to_storage_entry)
425 .collect());
426 }
427 super::common::run_storage(
428 move |js, budget| eval_js_string(&web_view, js, budget),
429 scope,
430 )
431 }
432
433 fn scripts(&mut self, page: PageHandle) -> EngineResult<Vec<crate::inspector::ScriptEntry>> {
434 let p = self.page_mut(page)?;
435 let web_view = p.web_view.clone();
436 super::common::run_scripts(move |js, budget| eval_js_string(&web_view, js, budget))
437 }
438
439 fn script_source(
440 &mut self,
441 page: PageHandle,
442 seq: u64,
443 ) -> EngineResult<Option<crate::inspector::ScriptSource>> {
444 let p = self.page_mut(page)?;
445 let web_view = p.web_view.clone();
446 super::common::run_script_source(
447 move |js, budget| eval_js_string(&web_view, js, budget),
448 seq,
449 )
450 }
451
452 fn dom(
453 &mut self,
454 page: PageHandle,
455 r: Ref,
456 extra_props: &[String],
457 ) -> EngineResult<Option<crate::inspector::DomDetail>> {
458 let p = self.page_mut(page)?;
459 let web_view = p.web_view.clone();
460 super::common::run_dom(
461 move |js, budget| eval_js_string(&web_view, js, budget),
462 r,
463 extra_props,
464 )
465 }
466
467 fn performance(
468 &mut self,
469 page: PageHandle,
470 ) -> EngineResult<crate::inspector::PerformanceMetrics> {
471 let p = self.page_mut(page)?;
472 let web_view = p.web_view.clone();
473 super::common::run_performance(move |js, budget| eval_js_string(&web_view, js, budget))
474 }
475
476 fn cookie_events(
477 &mut self,
478 page: PageHandle,
479 ) -> EngineResult<Vec<crate::inspector::CookieEvent>> {
480 let p = self.page_mut(page)?;
481 let web_view = p.web_view.clone();
482 let current = wpe_cookies::get_all_cookies(&web_view)?;
483 let previous = p.cookie_baseline.borrow().clone();
484 let mut seq = p.cookie_next_seq.borrow_mut();
485 let events = super::common::diff_cookies(previous.as_deref(), ¤t, &mut seq);
486 *p.cookie_baseline.borrow_mut() = Some(current);
487 Ok(events)
488 }
489
490 fn cursor_op(&mut self, page: PageHandle, op: CursorOp, mode: InputMode) -> EngineResult<()> {
491 let p = self.page_mut(page)?;
492 let dispatcher = super::wpe_input::dispatcher()?;
493 let humanize_mode = match mode {
494 InputMode::Human => vs_humanize::InputMode::Human,
495 InputMode::Careful => vs_humanize::InputMode::Careful,
496 InputMode::Robotic => vs_humanize::InputMode::Robotic,
497 };
498 let (origin_x, origin_y) = (0.0_f64, 0.0_f64);
506 let start = p.last_mouse.get();
507 let seed = humanize_seed(op);
508 let landed = match op {
509 CursorOp::MoveTo { x, y } | CursorOp::HoverAt { x, y } => {
510 cursor_move_along_path(dispatcher, start, vs_humanize::Point { x, y },
511 (origin_x, origin_y), humanize_mode, seed, false)?
512 }
513 CursorOp::ClickAt { x, y } => {
514 let landed = cursor_move_along_path(dispatcher, start, vs_humanize::Point { x, y },
515 (origin_x, origin_y), humanize_mode, seed, false)?;
516 cursor_press_release(dispatcher, landed, (origin_x, origin_y))?;
517 landed
518 }
519 CursorOp::Drag { x1, y1, x2, y2 } => {
520 let start_pt = vs_humanize::Point { x: x1, y: y1 };
521 let target = vs_humanize::Point { x: x2, y: y2 };
522 let pre = cursor_move_along_path(dispatcher, start, start_pt,
523 (origin_x, origin_y), humanize_mode, seed, false)?;
524 dispatcher.dispatch(super::wpe_input::InputEvent::Press(
525 super::wpe_input::Button::Left,
526 ))?;
527 std::thread::sleep(Duration::from_millis(15));
528 let landed = cursor_move_along_path(dispatcher, pre, target,
529 (origin_x, origin_y), humanize_mode, seed, true)?;
530 dispatcher.dispatch(super::wpe_input::InputEvent::Release(
531 super::wpe_input::Button::Left,
532 ))?;
533 dispatcher.flush()?;
534 let html5_js = super::common::build_html5_drag_js(x1, y1, x2, y2);
538 let web_view = p.web_view.clone();
539 let _ = eval_js_string(&web_view, &html5_js, Duration::from_secs(2));
540 landed
541 }
542 };
543 p.last_mouse.set(landed);
544 Ok(())
545 }
546
547
548 fn capabilities(&self) -> EngineCapabilities {
549 let any_inspector = self.pages.values().any(|p| p.inspector_installed);
550 EngineCapabilities {
551 renders: true,
552 honors_viewport: true,
553 measures_layout: true,
554 persists_auth: true,
555 inspector_console: any_inspector,
556 inspector_network: any_inspector,
557 inspector_cookie_events: true,
558 name: "wpe",
559 version: "Linux WebKitGTK 6 (webkit6)",
560 }
561 }
562}
563
564fn humanize_seed(op: CursorOp) -> u64 {
575 let (a, b, c, d) = match op {
578 CursorOp::MoveTo { x, y } | CursorOp::HoverAt { x, y } | CursorOp::ClickAt { x, y } => {
579 (x, y, 0.0, 0.0)
580 }
581 CursorOp::Drag { x1, y1, x2, y2 } => (x1, y1, x2, y2),
582 };
583 let bits = |v: f64| v.to_bits();
584 bits(a).wrapping_mul(0x9E37_79B9_7F4A_7C15)
585 ^ bits(b).wrapping_mul(0xBF58_476D_1CE4_E5B9)
586 ^ bits(c).wrapping_mul(0x94D0_49BB_1331_11EB)
587 ^ bits(d)
588}
589
590fn cursor_move_along_path(
598 dispatcher: &dyn super::wpe_input::InputDispatcher,
599 start: vs_humanize::Point,
600 end: vs_humanize::Point,
601 origin: (f64, f64),
602 mode: vs_humanize::InputMode,
603 seed: u64,
604 _button_down: bool,
605) -> EngineResult<vs_humanize::Point> {
606 let path = vs_humanize::mouse_path(start, end, mode, seed);
607 let mut prev_ms: u128 = 0;
608 for step in &path {
609 if step.kind != vs_humanize::MouseStepKind::Move {
610 continue;
611 }
612 let screen = super::wpe_input::ScreenPoint {
613 #[allow(clippy::cast_possible_truncation)]
614 x: (origin.0 + step.point.x).round() as i32,
615 #[allow(clippy::cast_possible_truncation)]
616 y: (origin.1 + step.point.y).round() as i32,
617 };
618 dispatcher.dispatch(super::wpe_input::InputEvent::Move(screen))?;
619 let dt = step.at.as_millis().saturating_sub(prev_ms);
620 if dt > 0 {
621 std::thread::sleep(Duration::from_millis(u64::try_from(dt).unwrap_or(0)));
622 }
623 prev_ms = step.at.as_millis();
624 }
625 let final_pt = super::wpe_input::ScreenPoint {
627 #[allow(clippy::cast_possible_truncation)]
628 x: (origin.0 + end.x).round() as i32,
629 #[allow(clippy::cast_possible_truncation)]
630 y: (origin.1 + end.y).round() as i32,
631 };
632 dispatcher.dispatch(super::wpe_input::InputEvent::Move(final_pt))?;
633 dispatcher.flush()?;
634 Ok(end)
635}
636
637fn cursor_press_release(
641 dispatcher: &dyn super::wpe_input::InputDispatcher,
642 _at: vs_humanize::Point,
643 _origin: (f64, f64),
644) -> EngineResult<()> {
645 dispatcher.dispatch(super::wpe_input::InputEvent::Press(
646 super::wpe_input::Button::Left,
647 ))?;
648 std::thread::sleep(Duration::from_millis(15));
649 dispatcher.dispatch(super::wpe_input::InputEvent::Release(
650 super::wpe_input::Button::Left,
651 ))?;
652 dispatcher.flush()?;
653 std::thread::sleep(Duration::from_millis(30));
654 Ok(())
655}
656
657fn eval_js_string(web_view: &WebView, js: &str, budget: Duration) -> EngineResult<String> {
658 let slot: Rc<RefCell<Option<Result<String, String>>>> = Rc::new(RefCell::new(None));
659 let slot_for_cb = slot.clone();
660
661 let cancellable = webkit6::gio::Cancellable::new();
662 web_view.evaluate_javascript(
663 js,
664 None,
665 None,
666 Some(&cancellable),
667 move |result| match result {
668 Ok(value) => {
669 let s = value.to_string();
670 *slot_for_cb.borrow_mut() = Some(Ok(s));
671 }
672 Err(err) => {
673 *slot_for_cb.borrow_mut() = Some(Err(err.to_string()));
674 }
675 },
676 );
677
678 let slot_check = slot.clone();
679 let ok = run_loop_until(move || slot_check.borrow().is_some(), budget);
680 if !ok {
681 return Err(EngineError::Timeout {
682 budget,
683 primitive: "eval",
684 });
685 }
686 let result = slot.borrow_mut().take();
687 match result {
688 Some(Ok(s)) => Ok(s),
689 Some(Err(msg)) => Err(EngineError::Other(format!("eval failed: {msg}"))),
690 None => unreachable!(),
691 }
692}
693
694fn install_inspector(web_view: &WebView, slots: &super::inspector_bridge::InspectorSlots) -> bool {
699 use super::inspector_bridge::{
700 self, InspectorSlots, NetworkIngestSlot, CONSOLE_HANDLER, NETWORK_HANDLER,
701 };
702
703 if std::env::var_os("VS_DISABLE_INSPECTOR").is_some() {
704 return false;
705 }
706
707 let manager = web_view.user_content_manager().expect(
708 "WebKitGTK WebView is missing a UserContentManager — should not happen on a default WebView",
709 );
710
711 let script = UserScript::new(
712 inspector_bridge::SCRIPT,
713 UserContentInjectedFrames::AllFrames,
714 UserScriptInjectionTime::Start,
715 &[],
716 &[],
717 );
718 manager.add_script(&script);
719
720 if !manager.register_script_message_handler(CONSOLE_HANDLER, None) {
721 return false;
722 }
723 if !manager.register_script_message_handler(NETWORK_HANDLER, None) {
724 return false;
725 }
726
727 let console_slots: InspectorSlots = slots.clone();
728 manager.connect_script_message_received(Some(CONSOLE_HANDLER), move |_, value| {
729 let json = value.to_string();
730 let mut buf = console_slots.console.borrow_mut();
731 inspector_bridge::ingest_console(&mut buf, &json);
732 });
733 let network_slots: InspectorSlots = slots.clone();
734 manager.connect_script_message_received(Some(NETWORK_HANDLER), move |_, value| {
735 let json = value.to_string();
736 let mut entries = network_slots.network.borrow_mut();
737 let mut details = network_slots.details.borrow_mut();
738 let mut pending = network_slots.pending.borrow_mut();
739 inspector_bridge::ingest_network(
740 NetworkIngestSlot {
741 entries: &mut entries,
742 details: &mut details,
743 pending: &mut pending,
744 },
745 &json,
746 );
747 });
748 true
749}
750
751mod wpe_cookies {
760 use std::cell::RefCell;
761 use std::rc::Rc;
762 use std::time::Duration;
763
764 use webkit6::gio;
765 use webkit6::glib;
766 use webkit6::prelude::*;
767 use webkit6::soup;
768 use webkit6::WebView;
769
770 use crate::backend::auth::CookieData;
771 use crate::engine::{EngineError, EngineResult};
772
773 use super::run_loop_until;
774
775 const ASYNC_BUDGET: Duration = Duration::from_secs(5);
776
777 fn cookie_manager(web_view: &WebView) -> EngineResult<webkit6::CookieManager> {
778 let session = WebViewExt::network_session(web_view).ok_or_else(|| {
779 EngineError::Other("WebView has no NetworkSession; cookies unavailable".into())
780 })?;
781 session
782 .cookie_manager()
783 .ok_or_else(|| EngineError::Other("NetworkSession has no CookieManager".into()))
784 }
785
786 pub(super) fn get_all_cookies(web_view: &WebView) -> EngineResult<Vec<CookieData>> {
787 let manager = cookie_manager(web_view)?;
788 let slot: Rc<RefCell<Option<Vec<CookieData>>>> = Rc::new(RefCell::new(None));
789 let slot_cb = slot.clone();
790 manager.all_cookies(None::<&gio::Cancellable>, move |result| {
791 let cookies = match result {
792 Ok(v) => v.into_iter().map(|mut c| serialize(&mut c)).collect(),
793 Err(_) => Vec::new(),
794 };
795 *slot_cb.borrow_mut() = Some(cookies);
796 });
797 let check = slot.clone();
798 let ok = run_loop_until(move || check.borrow().is_some(), ASYNC_BUDGET);
799 if !ok {
800 return Err(EngineError::Timeout {
801 budget: ASYNC_BUDGET,
802 primitive: "save_auth (all_cookies)",
803 });
804 }
805 let result = slot.borrow_mut().take().unwrap_or_default();
806 Ok(result)
807 }
808
809 pub(super) fn set_cookies(web_view: &WebView, cookies: &[CookieData]) -> EngineResult<()> {
810 let manager = cookie_manager(web_view)?;
811 for c in cookies {
812 if c.name.is_empty() || c.domain.is_empty() {
813 continue;
814 }
815 let path = if c.path.is_empty() {
816 "/"
817 } else {
818 c.path.as_str()
819 };
820 let mut cookie = soup::Cookie::new(&c.name, &c.value, &c.domain, path, -1);
823 cookie.set_secure(c.secure);
824 cookie.set_http_only(c.http_only);
825 if let Some(unix) = c.expires_unix {
826 if let Ok(dt) = glib::DateTime::from_unix_utc(unix) {
827 cookie.set_expires(&dt);
828 }
829 }
830 if let Some(ss) = c.same_site.as_deref() {
831 let policy = match ss {
832 "Strict" => soup::SameSitePolicy::Strict,
833 "None" => soup::SameSitePolicy::None,
834 _ => soup::SameSitePolicy::Lax,
835 };
836 cookie.set_same_site_policy(policy);
837 }
838
839 let done: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
840 let done_cb = done.clone();
841 manager.add_cookie(&cookie, None::<&gio::Cancellable>, move |_| {
842 *done_cb.borrow_mut() = true;
843 });
844 let check = done.clone();
845 let ok = run_loop_until(move || *check.borrow(), ASYNC_BUDGET);
846 if !ok {
847 return Err(EngineError::Timeout {
848 budget: ASYNC_BUDGET,
849 primitive: "load_auth (add_cookie)",
850 });
851 }
852 }
853 Ok(())
854 }
855
856 fn serialize(c: &mut soup::Cookie) -> CookieData {
857 CookieData {
858 name: c.name().map(|s| s.to_string()).unwrap_or_default(),
859 value: c.value().map(|s| s.to_string()).unwrap_or_default(),
860 domain: c.domain().map(|s| s.to_string()).unwrap_or_default(),
861 path: c.path().map(|s| s.to_string()).unwrap_or_default(),
862 expires_unix: c.expires().map(|dt| dt.to_unix()),
863 secure: c.is_secure(),
864 http_only: c.is_http_only(),
865 same_site: match c.same_site_policy() {
866 soup::SameSitePolicy::Strict => Some("Strict".to_string()),
867 soup::SameSitePolicy::None => Some("None".to_string()),
868 soup::SameSitePolicy::Lax => Some("Lax".to_string()),
869 _ => None,
870 },
871 }
872 }
873}