use crate::domains::network;
use crate::domains::{DispatchContext, DomainResult};
use crate::event::EventSender;
use crate::protocol::CdpError;
use serde_json::{Value, json};
pub async fn handle(method: &str, params: Option<Value>, ctx: &DispatchContext) -> DomainResult {
match method {
"enable" => enable(ctx),
"disable" => disable(ctx),
"navigate" => navigate(params, ctx).await,
"reload" => reload(params, ctx).await,
"getFrameTree" => get_frame_tree(ctx).await,
"getFrameMetrics" => get_frame_metrics(),
"captureScreenshot" => capture_screenshot(params, ctx).await,
"printToPDF" => print_to_pdf(params, ctx).await,
"getLifecycleEvents" => Ok(Some(json!({ "events": [] }))),
"setLifecycleEventsEnabled" => set_lifecycle_events_enabled(params, ctx),
_ => Err(CdpError {
code: -32601,
message: format!("Page.{} not implemented", method),
}),
}
}
fn enable(ctx: &DispatchContext) -> DomainResult {
ctx.events.set_page_enabled(true);
Ok(Some(json!({})))
}
fn disable(ctx: &DispatchContext) -> DomainResult {
ctx.events.set_page_enabled(false);
Ok(Some(json!({})))
}
fn set_lifecycle_events_enabled(params: Option<Value>, ctx: &DispatchContext) -> DomainResult {
let params = params.unwrap_or_default();
let enabled = params
.get("enabled")
.and_then(|v| v.as_bool())
.unwrap_or(false);
ctx.events.set_page_enabled(enabled);
Ok(Some(json!({})))
}
async fn navigate(params: Option<Value>, ctx: &DispatchContext) -> DomainResult {
let params = params.unwrap_or_default();
let url = params
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("about:blank");
let loader_id = format!("LID-{}", uuid::Uuid::new_v4().as_simple());
let request_id = format!("REQ-{}", uuid::Uuid::new_v4().as_simple());
let pre_timestamp = EventSender::timestamp_ms();
ctx.events.send_network_event(
"Network.requestWillBeSent",
json!({
"requestId": request_id,
"loaderId": loader_id,
"documentURL": url,
"request": {
"url": url,
"method": "GET",
"headers": {},
"initialPriority": "VeryHigh",
"urlFragment": "",
},
"timestamp": pre_timestamp,
"wallTime": pre_timestamp / 1000.0,
"initiator": { "type": "other" },
"type": "Document",
"frameId": "main",
"hasUserGesture": false,
}),
);
let mut guard = ctx.session.write().await;
match guard.navigate(url).await {
Ok(()) => {
let timestamp = EventSender::timestamp_ms();
let frame_id = guard
.page()
.map(|p| p.root_frame().id().to_string())
.unwrap_or_else(|| "main".to_string());
let final_url = guard
.current_url()
.map(|u| u.to_string())
.unwrap_or_else(|| url.to_string());
ctx.events.send_page_event(
"Page.frameNavigated",
json!({
"frame": {
"id": frame_id,
"loaderId": loader_id,
"url": final_url,
"domainAndRegistry": "",
"securityOrigin": final_url,
"mimeType": "text/html",
"adFrameStatus": { "adFrameType": "none" },
"secureContextType": "Secure",
"crossOriginIsolatedContextType": "NotIsolated",
},
"type": "Navigation"
}),
);
network::emit_response_events(
&ctx.events,
&request_id,
&final_url,
&loader_id,
200,
"text/html",
);
ctx.events.send_page_event(
"Page.domContentLoadedEventFired",
json!({ "timestamp": timestamp }),
);
ctx.events
.send_page_event("Page.loadEventFired", json!({ "timestamp": timestamp }));
Ok(Some(json!({
"frameId": frame_id,
"loaderId": loader_id,
"errorText": Value::Null
})))
}
Err(e) => Err(CdpError {
code: -32000,
message: format!("Navigation failed: {e}"),
}),
}
}
async fn reload(_params: Option<Value>, ctx: &DispatchContext) -> DomainResult {
let loader_id = format!("LID-{}", uuid::Uuid::new_v4().as_simple());
let request_id = format!("REQ-{}", uuid::Uuid::new_v4().as_simple());
let current_url = {
let guard = ctx.session.read().await;
guard
.current_url()
.map(|u| u.to_string())
.unwrap_or_else(|| "about:blank".to_string())
};
let pre_timestamp = EventSender::timestamp_ms();
ctx.events.send_network_event(
"Network.requestWillBeSent",
json!({
"requestId": request_id,
"loaderId": loader_id,
"documentURL": current_url,
"request": {
"url": current_url,
"method": "GET",
"headers": {},
"initialPriority": "VeryHigh",
"urlFragment": "",
},
"timestamp": pre_timestamp,
"wallTime": pre_timestamp / 1000.0,
"initiator": { "type": "other" },
"type": "Document",
"frameId": "main",
"hasUserGesture": false,
}),
);
let mut guard = ctx.session.write().await;
match guard.reload().await {
Ok(()) => {
let timestamp = EventSender::timestamp_ms();
let frame_id = guard
.page()
.map(|p| p.root_frame().id().to_string())
.unwrap_or_else(|| "main".to_string());
let final_url = guard
.current_url()
.map(|u| u.to_string())
.unwrap_or_else(|| "about:blank".to_string());
ctx.events.send_page_event(
"Page.frameNavigated",
json!({
"frame": {
"id": frame_id,
"loaderId": loader_id,
"url": final_url,
"mimeType": "text/html",
},
"type": "Navigation"
}),
);
network::emit_response_events(
&ctx.events,
&request_id,
&final_url,
&loader_id,
200,
"text/html",
);
ctx.events.send_page_event(
"Page.domContentLoadedEventFired",
json!({ "timestamp": timestamp }),
);
ctx.events
.send_page_event("Page.loadEventFired", json!({ "timestamp": timestamp }));
Ok(Some(json!({
"frameId": frame_id,
"loaderId": loader_id
})))
}
Err(e) => Err(CdpError {
code: -32000,
message: format!("Reload failed: {e}"),
}),
}
}
async fn get_frame_tree(ctx: &DispatchContext) -> DomainResult {
let guard = ctx.session.read().await;
match guard.page() {
Some(page) => {
let frame = page.root_frame();
let url = frame.url();
Ok(Some(json!({
"frameTree": {
"frame": {
"id": frame.id().to_string(),
"url": url.to_string(),
"securityOrigin": url.origin().unicode_serialization(),
"mimeType": "text/html"
},
"childFrames": []
}
})))
}
None => Ok(Some(json!({
"frameTree": {
"frame": {
"id": "main",
"url": "about:blank",
"securityOrigin": "",
"mimeType": "text/html"
},
"childFrames": []
}
}))),
}
}
fn get_frame_metrics() -> DomainResult {
Ok(Some(json!({
"layoutViewport": {
"pageX": 0,
"pageY": 0,
"clientWidth": 1280,
"clientHeight": 720
},
"visualViewport": {
"offsetX": 0,
"offsetY": 0,
"pageX": 0,
"pageY": 0,
"clientWidth": 1280,
"clientHeight": 720,
"scale": 1,
"zoom": 1
},
"contentSize": {
"width": 1280,
"height": 720
}
})))
}
async fn capture_screenshot(params: Option<Value>, ctx: &DispatchContext) -> DomainResult {
let params = params.unwrap_or_default();
let _format = params
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("png");
let viewport_width = params
.get("clip")
.and_then(|v| v.get("width"))
.and_then(|v| v.as_f64())
.unwrap_or(1280.0) as u32;
let guard = ctx.session.read().await;
let png_bytes: Vec<u8> = match guard.page() {
Some(page) => page
.to_screenshot_png(viewport_width.max(64))
.unwrap_or_else(|_| {
oxibrowser_core::css::text_to_png("", viewport_width.max(64)).unwrap_or_default()
}),
None => oxibrowser_core::css::text_to_png("", viewport_width.max(64)).unwrap_or_default(),
};
use base64::Engine;
let data = base64::engine::general_purpose::STANDARD.encode(&png_bytes);
Ok(Some(json!({
"data": data,
"metadata": {
"pageScaleFactor": 1,
"deviceWidth": viewport_width,
"deviceHeight": 720
}
})))
}
async fn print_to_pdf(_params: Option<Value>, _ctx: &DispatchContext) -> DomainResult {
Ok(Some(json!({
"data": "",
"stream": ""
})))
}