1use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use ferridriver::context::ContextRef;
7use rquickjs::function::Opt;
8use rquickjs::{Ctx, JsLifetime, Value, class::Trace};
9use rustc_hash::FxHashMap;
10
11use crate::bindings::convert::{FerriResultExt, init_script_from_js, serde_from_js, serde_to_js};
12use crate::bindings::page::{call_predicate_truthy, url_value_to_matcher, with_page_callbacks};
13
14#[derive(JsLifetime, Trace)]
15#[rquickjs::class(rename = "BrowserContext")]
16pub struct BrowserContextJs {
17 #[qjs(skip_trace)]
18 inner: Arc<ContextRef>,
19 #[qjs(skip_trace)]
23 next_route_id: Arc<AtomicU64>,
24 #[qjs(skip_trace)]
28 route_matchers: Arc<std::sync::Mutex<FxHashMap<u64, ferridriver::url_matcher::UrlMatcher>>>,
29}
30
31impl BrowserContextJs {
32 #[must_use]
33 pub fn new(inner: Arc<ContextRef>) -> Self {
34 static CONTEXT_ROUTE_BASE: AtomicU64 = AtomicU64::new(1 << 48);
39 Self {
40 inner,
41 next_route_id: Arc::new(AtomicU64::new(CONTEXT_ROUTE_BASE.fetch_add(1 << 20, Ordering::Relaxed))),
42 route_matchers: Arc::new(std::sync::Mutex::new(FxHashMap::default())),
43 }
44 }
45}
46
47#[rquickjs::methods]
48impl BrowserContextJs {
49 #[qjs(rename = "cookies")]
56 pub async fn cookies<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
57 let cookies = self.inner.cookies().await.into_js()?;
58 serde_to_js(&ctx, &cookies)
59 }
60
61 #[qjs(rename = "addCookies")]
67 pub async fn add_cookies<'js>(&self, ctx: Ctx<'js>, cookies: Value<'js>) -> rquickjs::Result<()> {
68 let parsed: Vec<ferridriver::backend::SetCookieParams> = serde_from_js(&ctx, cookies)?;
69 let cookies: Vec<ferridriver::backend::CookieData> = parsed.into_iter().map(Into::into).collect();
70 self.inner.add_cookies(cookies).await.into_js()
71 }
72
73 #[qjs(rename = "clearCookies")]
80 pub async fn clear_cookies<'js>(
81 &self,
82 ctx: rquickjs::Ctx<'js>,
83 options: rquickjs::function::Opt<rquickjs::Value<'js>>,
84 ) -> rquickjs::Result<()> {
85 match options.0 {
86 None => self.inner.clear_cookies().await.into_js(),
87 Some(v) if v.is_undefined() || v.is_null() => self.inner.clear_cookies().await.into_js(),
88 Some(v) => {
89 #[derive(serde::Deserialize, Default)]
90 struct Filter {
91 name: Option<String>,
92 domain: Option<String>,
93 path: Option<String>,
94 }
95 let parsed: Filter = crate::bindings::convert::serde_from_js(&ctx, v)?;
96 let core = ferridriver::backend::ClearCookieOptions {
97 name: parsed.name,
98 domain: parsed.domain,
99 path: parsed.path,
100 };
101 self.inner.clear_cookies_filtered(&core).await.into_js()
102 },
103 }
104 }
105
106 #[qjs(rename = "deleteCookie")]
108 pub async fn delete_cookie(&self, name: String, domain: Opt<String>) -> rquickjs::Result<()> {
109 self.inner.delete_cookie(&name, domain.0.as_deref()).await.into_js()
110 }
111
112 #[qjs(rename = "storageState")]
120 pub async fn storage_state<'js>(&self, ctx: Ctx<'js>, options: Opt<Value<'js>>) -> rquickjs::Result<Value<'js>> {
121 #[derive(serde::Deserialize)]
122 #[serde(rename_all = "camelCase")]
123 struct JsStorageStateOptions {
124 path: Option<String>,
125 indexed_db: Option<bool>,
126 }
127 let core_opts = match options.0 {
128 Some(v) if !v.is_undefined() && !v.is_null() => {
129 let parsed: JsStorageStateOptions = serde_from_js(&ctx, v)?;
130 Some(ferridriver::options::StorageStateOptions {
131 path: parsed.path.map(std::path::PathBuf::from),
132 indexed_db: parsed.indexed_db,
133 })
134 },
135 _ => None,
136 };
137 let state = self.inner.storage_state(core_opts).await.into_js()?;
138 serde_to_js(&ctx, &state)
139 }
140
141 #[qjs(rename = "grantPermissions")]
146 pub async fn grant_permissions(&self, permissions: Vec<String>, origin: Opt<String>) -> rquickjs::Result<()> {
147 self
148 .inner
149 .grant_permissions(&permissions, origin.0.as_deref())
150 .await
151 .into_js()
152 }
153
154 #[qjs(rename = "clearPermissions")]
156 pub async fn clear_permissions(&self) -> rquickjs::Result<()> {
157 self.inner.clear_permissions().await.into_js()
158 }
159
160 #[qjs(rename = "setGeolocation")]
164 pub async fn set_geolocation(&self, latitude: f64, longitude: f64, accuracy: f64) -> rquickjs::Result<()> {
165 self
166 .inner
167 .set_geolocation(latitude, longitude, accuracy)
168 .await
169 .into_js()
170 }
171
172 #[qjs(rename = "setOffline")]
174 pub async fn set_offline(&self, offline: bool) -> rquickjs::Result<()> {
175 self.inner.set_offline(offline).await.into_js()
176 }
177
178 #[qjs(rename = "setHTTPCredentials")]
184 pub async fn set_http_credentials<'js>(&self, ctx: Ctx<'js>, credentials: Opt<Value<'js>>) -> rquickjs::Result<()> {
185 let creds = match credentials.0 {
186 None => None,
187 Some(v) if v.is_undefined() || v.is_null() => None,
188 Some(v) => {
189 #[derive(serde::Deserialize)]
190 #[serde(rename_all = "camelCase")]
191 struct JsCreds {
192 username: String,
193 password: String,
194 origin: Option<String>,
195 send: Option<String>,
196 }
197 let parsed: JsCreds = serde_from_js(&ctx, v)?;
198 Some(ferridriver::options::HttpCredentials {
199 username: parsed.username,
200 password: parsed.password,
201 origin: parsed.origin,
202 send: parsed.send.and_then(|s| match s.as_str() {
203 "always" => Some(ferridriver::options::HttpCredentialsSend::Always),
204 "unauthorized" => Some(ferridriver::options::HttpCredentialsSend::Unauthorized),
205 _ => None,
206 }),
207 })
208 },
209 };
210 self.inner.set_http_credentials(creds).await.into_js()
211 }
212
213 #[qjs(rename = "setExtraHTTPHeaders")]
217 pub async fn set_extra_http_headers<'js>(&self, ctx: Ctx<'js>, headers: Value<'js>) -> rquickjs::Result<()> {
218 let map: FxHashMap<String, String> = serde_from_js(&ctx, headers)?;
219 self.inner.set_extra_http_headers(&map).await.into_js()
220 }
221
222 #[qjs(rename = "route")]
232 pub async fn route<'js>(
233 &self,
234 ctx: Ctx<'js>,
235 url: Value<'js>,
236 handler: rquickjs::Function<'js>,
237 options: rquickjs::function::Opt<Value<'js>>,
238 ) -> rquickjs::Result<()> {
239 let times = crate::bindings::page::parse_route_times(&options)?;
240 let async_ctx = match ctx.userdata::<crate::engine::SessionAsyncCtx>() {
241 Some(ud) => ud.0.clone(),
242 None => {
243 return Err(rquickjs::Error::new_from_js_message(
244 "context.route",
245 "Error",
246 "context.route requires the script engine's AsyncContext".to_string(),
247 ));
248 },
249 };
250 let id = self.next_route_id.fetch_add(1, Ordering::Relaxed);
251 let saved_handler = rquickjs::Persistent::save(&ctx, handler);
252 with_page_callbacks(&ctx, |r| r.insert_route_handler(id, saved_handler))?;
253
254 let has_predicate = url.as_function().is_some();
255 let matcher = if let Some(pred) = url.as_function() {
256 let saved_pred = rquickjs::Persistent::save(&ctx, pred.clone());
257 with_page_callbacks(&ctx, |r| r.insert_route_pred(id, saved_pred))?;
258 let m = ferridriver::url_matcher::UrlMatcher::predicate(|_| true);
259 self
260 .route_matchers
261 .lock()
262 .unwrap_or_else(std::sync::PoisonError::into_inner)
263 .insert(id, m.clone());
264 m
265 } else {
266 url_value_to_matcher(&ctx, url)?
267 };
268
269 let rust_handler: ferridriver::route::RouteHandler = std::sync::Arc::new(move |route| {
270 let async_ctx = async_ctx.clone();
271 tokio::spawn(async move {
272 use rquickjs::class::Class;
273 let _: rquickjs::Result<()> = rquickjs::async_with!(async_ctx => |ctx| {
274 if has_predicate {
275 let pred = with_page_callbacks(&ctx, |r| r.get_route_pred(id))?
276 .ok_or_else(|| rquickjs::Error::new_from_js_message("context.route", "Error", "route predicate gone".to_string()))?
277 .restore(&ctx)?;
278 let url_ctor: rquickjs::function::Constructor<'_> = ctx.globals().get("URL")?;
279 let url_obj: rquickjs::Value<'_> = url_ctor.construct((route.request().url.clone(),))?;
280 if !call_predicate_truthy(&pred, url_obj, &ctx).await? {
281 route.continue_route(ferridriver::route::ContinueOverrides::default());
282 return Ok(());
283 }
284 }
285 let f = with_page_callbacks(&ctx, |r| r.get_route_handler(id))?
286 .ok_or_else(|| rquickjs::Error::new_from_js_message("context.route", "Error", "route handler gone".to_string()))?
287 .restore(&ctx)?;
288 let route_class = Class::instance(ctx.clone(), crate::bindings::network::RouteJs::new(route))?;
289 let _: rquickjs::Value<'_> = f.call((route_class,))?;
290 Ok(())
291 })
292 .await;
293 });
294 });
295
296 self.inner.route(matcher, rust_handler, times).await.into_js()?;
297 Ok(())
298 }
299
300 #[qjs(rename = "routeFromHAR")]
302 pub async fn route_from_har(&self, har: String, options: rquickjs::function::Opt<Value<'_>>) -> rquickjs::Result<()> {
303 let opts = crate::bindings::page::parse_har_options(&options)?;
304 self
305 .inner
306 .route_from_har(std::path::Path::new(&har), opts)
307 .await
308 .into_js()
309 }
310
311 #[qjs(rename = "unroute")]
315 pub async fn unroute<'js>(&self, ctx: Ctx<'js>, url: Value<'js>) -> rquickjs::Result<()> {
316 if let Some(pred) = url.as_function() {
317 let saved = with_page_callbacks(&ctx, |r| r.route_preds_snapshot())?;
318 let mut victims: Vec<u64> = Vec::new();
319 for (id, sp) in saved {
320 let stored = sp.restore(&ctx)?;
321 if stored.as_value() == pred.as_value() {
322 victims.push(id);
323 }
324 }
325 for id in victims {
326 let m = self
327 .route_matchers
328 .lock()
329 .unwrap_or_else(std::sync::PoisonError::into_inner)
330 .remove(&id);
331 if let Some(m) = m {
332 self.inner.unroute(&m).await.into_js()?;
333 }
334 with_page_callbacks(&ctx, |r| r.remove_route(id))?;
335 }
336 return Ok(());
337 }
338 let matcher = url_value_to_matcher(&ctx, url)?;
339 self.inner.unroute(&matcher).await.into_js()
340 }
341
342 #[qjs(rename = "addInitScript")]
351 pub async fn add_init_script<'js>(
352 &self,
353 ctx: Ctx<'js>,
354 script: Value<'js>,
355 arg: Opt<Value<'js>>,
356 ) -> rquickjs::Result<Value<'js>> {
357 let (init, arg_json) = init_script_from_js(&ctx, script, arg.0)?;
358 let disposable = self.inner.add_init_script(init, arg_json).await.into_js()?;
359 let instance =
360 rquickjs::class::Class::instance(ctx.clone(), crate::bindings::disposable::DisposableJs::new(disposable))?;
361 rquickjs::IntoJs::into_js(instance, &ctx)
362 }
363
364 #[qjs(rename = "setDefaultTimeout")]
371 pub fn set_default_timeout(&self, timeout: f64) {
372 self
373 .inner
374 .set_default_timeout(crate::bindings::convert::ms_f64_to_u64(timeout));
375 }
376
377 #[qjs(rename = "setDefaultNavigationTimeout")]
380 pub fn set_default_navigation_timeout(&self, timeout: f64) {
381 self
382 .inner
383 .set_default_navigation_timeout(crate::bindings::convert::ms_f64_to_u64(timeout));
384 }
385
386 #[qjs(rename = "name")]
390 pub fn name(&self) -> String {
391 self.inner.name().to_string()
392 }
393
394 #[qjs(rename = "browser")]
399 pub fn browser<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
400 use rquickjs::class::Class;
401 match self.inner.browser() {
402 Some(b) => {
403 let wrapper = crate::bindings::browser::BrowserJs::new(std::sync::Arc::new(b.clone()));
404 let instance = Class::instance(ctx.clone(), wrapper)?;
405 rquickjs::IntoJs::into_js(instance, &ctx)
406 },
407 None => Ok(Value::new_null(ctx)),
408 }
409 }
410
411 #[qjs(rename = "isClosed")]
414 pub fn is_closed(&self) -> bool {
415 self.inner.is_closed()
416 }
417
418 #[qjs(rename = "close")]
420 pub async fn close(&self) -> rquickjs::Result<()> {
421 self.inner.close().await.into_js()
422 }
423
424 #[qjs(rename = "newPage")]
433 pub async fn new_page<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
434 use rquickjs::class::Class;
435 let page = self.inner.new_page().await.into_js()?;
436 let wrapper = crate::bindings::page::pagejs_for_ctx(&ctx, page);
437 let instance = Class::instance(ctx.clone(), wrapper)?;
438 rquickjs::IntoJs::into_js(instance, &ctx)
439 }
440
441 #[qjs(rename = "setRecordVideo")]
449 pub async fn set_record_video<'js>(&self, ctx: Ctx<'js>, options: Value<'js>) -> rquickjs::Result<()> {
450 #[derive(serde::Deserialize)]
451 #[serde(rename_all = "camelCase")]
452 struct JsRecordVideoOptions {
453 dir: String,
454 size: Option<JsVideoSize>,
455 }
456 #[derive(serde::Deserialize)]
457 struct JsVideoSize {
458 width: f64,
459 height: f64,
460 }
461 let parsed: JsRecordVideoOptions = serde_from_js(&ctx, options)?;
462 let opts = ferridriver::options::RecordVideoOptions {
463 dir: std::path::PathBuf::from(parsed.dir),
464 size: parsed.size.map(|s| ferridriver::options::VideoSize {
465 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
466 width: s.width.max(0.0) as u32,
467 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
468 height: s.height.max(0.0) as u32,
469 }),
470 };
471 self.inner.set_record_video(opts).await.into_js()
472 }
473
474 #[qjs(rename = "exposeBinding")]
487 pub async fn expose_binding<'js>(
488 &self,
489 ctx: Ctx<'js>,
490 name: String,
491 callback: rquickjs::Function<'js>,
492 ) -> rquickjs::Result<Value<'js>> {
493 let binding = self.make_binding(&ctx, &name, callback, true)?;
494 self.inner.expose_binding(&name, binding).await.into_js()?;
495 self.make_disposable(&ctx, name)
496 }
497
498 #[qjs(rename = "exposeFunction")]
504 pub async fn expose_function<'js>(
505 &self,
506 ctx: Ctx<'js>,
507 name: String,
508 callback: rquickjs::Function<'js>,
509 ) -> rquickjs::Result<Value<'js>> {
510 let binding = self.make_binding(&ctx, &name, callback, false)?;
511 self.inner.expose_binding(&name, binding).await.into_js()?;
512 self.make_disposable(&ctx, name)
513 }
514
515 #[qjs(rename = "waitForEvent")]
521 pub async fn wait_for_event<'js>(
522 &self,
523 ctx: Ctx<'js>,
524 event: String,
525 timeout_ms: Opt<f64>,
526 ) -> rquickjs::Result<Value<'js>> {
527 use rquickjs::class::Class;
528 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
529 let timeout = timeout_ms.0.unwrap_or(30_000.0) as u64;
530 let ev = self
531 .inner
532 .wait_for_event(&event, timeout)
533 .await
534 .map_err(|e| rquickjs::Error::new_from_js_message("BrowserContext.waitForEvent", "Error", e.to_string()))?;
535 match ev {
536 ferridriver::events::ContextEvent::WebError(err) => {
537 let wrapper = crate::bindings::web_error::WebErrorJs::new(err);
538 let instance = Class::instance(ctx.clone(), wrapper)?;
539 rquickjs::IntoJs::into_js(instance, &ctx)
540 },
541 }
542 }
543}
544
545impl BrowserContextJs {
546 fn make_binding<'js>(
553 &self,
554 ctx: &Ctx<'js>,
555 name: &str,
556 callback: rquickjs::Function<'js>,
557 with_source: bool,
558 ) -> rquickjs::Result<ferridriver::ExposedBinding> {
559 let async_ctx = match ctx.userdata::<crate::engine::SessionAsyncCtx>() {
560 Some(ud) => ud.0.clone(),
561 None => {
562 return Err(rquickjs::Error::new_from_js_message(
563 "BrowserContext.exposeBinding",
564 "Error",
565 "exposeBinding requires the script engine's AsyncContext".to_string(),
566 ));
567 },
568 };
569 let saved = rquickjs::Persistent::save(ctx, callback);
570 crate::bindings::page::insert_exposed_callback(ctx, name.to_string(), saved)?;
571
572 let name = name.to_string();
573 let binding: ferridriver::ExposedBinding = Arc::new(move |source, args| {
574 let async_ctx = async_ctx.clone();
575 let name = name.clone();
576 Box::pin(async move {
577 let out: rquickjs::Result<serde_json::Value> = rquickjs::async_with!(async_ctx => |ctx| {
578 let f = crate::bindings::page::get_exposed_callback(&ctx, &name)?
579 .ok_or_else(|| {
580 rquickjs::Error::new_from_js_message(
581 "BrowserContext.exposeBinding",
582 "Error",
583 "exposed callback gone".to_string(),
584 )
585 })?
586 .restore(&ctx)?;
587 let mut call_args = rquickjs::function::Args::new_unsized(ctx.clone());
591 if with_source {
592 let src = rquickjs::Object::new(ctx.clone())?;
593 src.set("context", source.context.clone())?;
594 src.set("page", source.page.clone())?;
595 src.set("frame", source.frame.clone())?;
596 call_args.push_arg(src)?;
597 }
598 for v in &args {
599 call_args.push_arg(crate::bindings::convert::json_to_js(&ctx, v)?)?;
603 }
604 let mp: rquickjs::promise::MaybePromise<'_> = call_args.apply(&f)?;
605 let res = mp.into_future::<rquickjs::Value<'_>>().await?;
606 let json = match ctx.json_stringify(res)? {
607 Some(s) => serde_json::from_str(&s.to_string()?).unwrap_or(serde_json::Value::Null),
608 None => serde_json::Value::Null,
609 };
610 Ok(json)
611 })
612 .await;
613 out.unwrap_or(serde_json::Value::Null)
614 })
615 });
616 Ok(binding)
617 }
618
619 fn make_disposable<'js>(&self, ctx: &Ctx<'js>, name: String) -> rquickjs::Result<Value<'js>> {
624 let obj = rquickjs::Object::new(ctx.clone())?;
625 let inner = self.inner.clone();
626 let dispose = rquickjs::Function::new(
627 ctx.clone(),
628 rquickjs::prelude::Async(move || {
629 let inner = inner.clone();
630 let name = name.clone();
631 async move {
638 let _ = inner.remove_exposed_binding(&name).await;
639 }
640 }),
641 )?;
642 obj.set("dispose", dispose)?;
643 rquickjs::IntoJs::into_js(obj, ctx)
644 }
645}