use crate::{LogLevel, WebViewError, WebViewInputError, WebViewScriptError};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug)]
pub enum SchemeOutcome {
Handled(WebResourceResponse),
PassThrough,
}
pub(crate) type AsyncSchemeFuture = Pin<Box<dyn Future<Output = SchemeOutcome> + Send + 'static>>;
pub(crate) type AsyncSchemeHandler =
Arc<dyn Fn(http::Request<Vec<u8>>) -> AsyncSchemeFuture + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationPolicy {
Allow,
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationRequest {
pub url: String,
pub has_user_gesture: bool,
pub is_main_frame: bool,
}
impl NavigationRequest {
pub fn new(url: impl Into<String>, has_user_gesture: bool, is_main_frame: bool) -> Self {
Self {
url: url.into(),
has_user_gesture,
is_main_frame,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NewWindowPolicy {
LoadInSelf,
Cancel,
}
pub type NavigationHandler = Box<dyn Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync>;
pub type NewWindowHandler = Box<dyn Fn(&str) -> NewWindowPolicy + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserAgentOverride {
Default,
Custom(String),
}
impl UserAgentOverride {
pub(crate) fn validate(&self) -> Result<(), WebViewError> {
if let Self::Custom(value) = self
&& value.trim().is_empty()
{
return Err(WebViewError::WebView(
"custom user-agent override must not be empty".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod user_agent_override_tests {
use super::*;
#[test]
fn custom_user_agent_must_not_be_blank() {
assert!(UserAgentOverride::Custom(String::new()).validate().is_err());
assert!(UserAgentOverride::Custom(" ".into()).validate().is_err());
assert!(
UserAgentOverride::Custom("Mozilla/5.0 valid".into())
.validate()
.is_ok()
);
assert!(UserAgentOverride::Default.validate().is_ok());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DownloadRequest {
pub url: String,
pub user_agent: Option<String>,
pub content_disposition: Option<String>,
pub mime_type: Option<String>,
pub content_length: Option<u64>,
pub suggested_filename: Option<String>,
pub source_page_url: Option<String>,
pub cookie: Option<String>,
}
pub type DownloadHandler = Box<dyn Fn(DownloadRequest) + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum WebViewCookieSameSite {
Lax,
Strict,
None,
}
impl WebViewCookieSameSite {
pub fn as_str(self) -> &'static str {
match self {
Self::Lax => "lax",
Self::Strict => "strict",
Self::None => "none",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebViewCookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
#[serde(default, skip_serializing_if = "is_false")]
pub host_only: bool,
#[serde(default)]
pub secure: bool,
#[serde(default)]
pub http_only: bool,
#[serde(default)]
pub session: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_unix_ms: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub same_site: Option<WebViewCookieSameSite>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebViewCookieSetRequest {
#[serde(default)]
pub url: String,
pub name: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default = "default_cookie_path")]
pub path: String,
#[serde(default)]
pub secure: bool,
#[serde(default)]
pub http_only: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_unix_ms: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub same_site: Option<WebViewCookieSameSite>,
}
fn default_cookie_path() -> String {
"/".to_string()
}
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChooserRequest {
pub accept_types: Vec<String>,
pub allow_multiple: bool,
pub allow_directories: bool,
pub capture: bool,
pub source_page_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChooserFile {
pub path: Option<String>,
pub uri: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileChooserResponse {
Cancel,
Error(String),
Files(Vec<FileChooserFile>),
}
#[derive(Debug)]
pub enum WebResourceBody {
Path(PathBuf),
Pipe(SystemPipeReader),
Bytes(Vec<u8>),
}
#[derive(Debug)]
pub struct SystemPipeReader {
#[cfg(unix)]
fd: std::os::fd::RawFd,
#[cfg(windows)]
handle: std::os::windows::io::RawHandle,
}
impl SystemPipeReader {
#[cfg(unix)]
pub fn into_raw_fd(self) -> std::os::fd::RawFd {
self.fd
}
#[cfg(unix)]
pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
Self { fd }
}
#[cfg(unix)]
pub fn into_file(self) -> std::fs::File {
use std::os::fd::FromRawFd;
unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
}
#[cfg(windows)]
pub fn into_raw_handle(self) -> std::os::windows::io::RawHandle {
self.handle
}
#[cfg(windows)]
pub unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Self {
Self { handle }
}
#[cfg(windows)]
pub fn into_file(self) -> std::fs::File {
use std::os::windows::io::FromRawHandle;
unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
}
}
#[async_trait]
pub trait WebViewController: Send + Sync {
fn load_url(&self, url: &str) -> Result<(), WebViewError>;
fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
async fn current_url(&self) -> Result<Option<String>, WebViewError> {
Err(WebViewError::WebView(
"current_url is not implemented for this platform".to_string(),
))
}
fn post_message(&self, message: &str) -> Result<(), WebViewError>;
fn clear_browsing_data(&self) -> Result<(), WebViewError>;
fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError>;
fn reload(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"reload is not implemented for this platform".to_string(),
))
}
fn go_back(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"go_back is not implemented for this platform".to_string(),
))
}
fn go_forward(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"go_forward is not implemented for this platform".to_string(),
))
}
async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
Err(WebViewError::WebView(
"cookie store is not implemented for this platform".to_string(),
))
}
async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"cookie store is not implemented for this platform".to_string(),
))
}
async fn delete_cookie(
&self,
_name: &str,
_domain: &str,
_path: &str,
) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"cookie store is not implemented for this platform".to_string(),
))
}
async fn clear_cookies(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"cookie store is not implemented for this platform".to_string(),
))
}
async fn clear_site_data(
&self,
_url: &str,
_options: ClearSiteDataOptions,
) -> Result<ClearSiteDataResult, WebViewError> {
Err(WebViewError::WebView(
"site-scoped data clearing is not implemented for this platform".to_string(),
))
}
async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
Err(WebViewError::WebView(
"screenshot is not implemented for this platform".to_string(),
))
}
async fn start_network_capture(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"network capture is not implemented for this platform".to_string(),
))
}
async fn stop_network_capture(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"network capture is not implemented for this platform".to_string(),
))
}
async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
Err(WebViewError::WebView(
"network capture is not implemented for this platform".to_string(),
))
}
async fn clear_network_capture(&self) -> Result<(), WebViewError> {
Err(WebViewError::WebView(
"network capture is not implemented for this platform".to_string(),
))
}
}
#[derive(Debug, Clone, Copy)]
pub struct ClearSiteDataOptions {
pub cache: bool,
pub site_data: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ClearSiteDataResult {
pub cache_cleared: bool,
pub site_data_cleared: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkEntry {
pub request_id: String,
pub url: String,
pub method: String,
pub resource_type: Option<String>,
pub request_headers: Vec<(String, String)>,
pub request_body: Option<String>,
pub status: Option<u16>,
pub response_headers: Vec<(String, String)>,
pub mime_type: Option<String>,
pub response_body: NetworkBody,
pub from_cache: bool,
pub failed: Option<String>,
pub wall_time: Option<f64>,
pub started: f64,
pub finished: Option<f64>,
}
impl NetworkEntry {
pub fn duration_ms(&self) -> Option<f64> {
self.finished
.filter(|finished| *finished >= self.started)
.map(|finished| (finished - self.started) * 1000.0)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum NetworkBody {
#[default]
None,
Text { text: String },
Base64 { base64: String },
Skipped { reason: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NetworkCaptureSnapshot {
pub entries: Vec<NetworkEntry>,
pub dropped: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClickOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TypeOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
#[serde(default)]
pub replace: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FillOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PressOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScrollOptions;
#[async_trait]
pub trait WebViewInputController: WebViewController {
async fn click(
&self,
_selector: &str,
_options: ClickOptions,
) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
async fn type_text(
&self,
_selector: &str,
_text: &str,
_options: TypeOptions,
) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
async fn fill(
&self,
_selector: &str,
_text: &str,
_options: FillOptions,
) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
async fn scroll(
&self,
_dx: f64,
_dy: f64,
_options: ScrollOptions,
) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
async fn scroll_to(
&self,
_selector: &str,
_options: ScrollOptions,
) -> Result<(), WebViewInputError> {
Err(WebViewInputError::Unsupported(
"input control is not implemented for this platform",
))
}
}
#[derive(Debug, Clone, Copy)]
pub struct LoadDataRequest<'a> {
pub data: &'a str,
pub base_url: &'a str,
pub history_url: Option<&'a str>,
}
impl<'a> LoadDataRequest<'a> {
pub fn new(data: &'a str, base_url: &'a str) -> Self {
Self {
data,
base_url,
history_url: None,
}
}
pub fn with_history_url(mut self, history_url: &'a str) -> Self {
self.history_url = Some(history_url);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadErrorKind {
Dns,
Network,
Timeout,
Security,
InvalidUrl,
NotFound,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadError {
pub failing_url: Option<String>,
pub kind: LoadErrorKind,
pub description: String,
}
pub trait WebViewDelegate: Send + Sync {
fn on_navigation_event(&self, event: crate::events::NavigationEvent);
fn on_webview_state_change(&self, _change: crate::events::WebViewStateChange) {}
fn handle_post_message(&self, msg: String);
fn handle_native_component_message(&self, _message_json: String) {}
fn log(&self, level: LogLevel, message: &str);
}
#[derive(Debug)]
pub struct WebResourceResponse {
parts: http::response::Parts,
body: WebResourceBody,
}
impl From<Option<WebResourceResponse>> for SchemeOutcome {
fn from(value: Option<WebResourceResponse>) -> Self {
match value {
Some(response) => SchemeOutcome::Handled(response),
None => SchemeOutcome::PassThrough,
}
}
}
impl WebResourceResponse {
pub fn parts(&self) -> &http::response::Parts {
&self.parts
}
pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
(self.parts, self.body)
}
}
impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
fn from(value: (http::response::Parts, PathBuf)) -> Self {
WebResourceResponse {
parts: value.0,
body: WebResourceBody::Path(value.1),
}
}
}
impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
WebResourceResponse {
parts: value.0,
body: WebResourceBody::Pipe(value.1),
}
}
}
impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
WebResourceResponse {
parts: value.0,
body: WebResourceBody::Bytes(value.1),
}
}
}
impl WebResourceResponse {
fn response_parts_with_status(status: u16) -> http::response::Parts {
let response = match http::Response::builder().status(status).body(()) {
Ok(response) => response,
Err(_) => http::Response::new(()),
};
let (parts, _) = response.into_parts();
parts
}
pub fn file(path: impl Into<PathBuf>) -> Self {
let path = path.into();
let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
let mut parts = Self::response_parts_with_status(200);
if let Some(len) = content_length {
parts
.headers
.insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
}
Self {
parts,
body: WebResourceBody::Path(path),
}
}
pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
let data = data.into();
let len = data.len();
let mut parts = Self::response_parts_with_status(200);
parts
.headers
.insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
Self {
parts,
body: WebResourceBody::Bytes(data),
}
}
pub fn stream(reader: SystemPipeReader) -> Self {
let parts = Self::response_parts_with_status(200);
Self {
parts,
body: WebResourceBody::Pipe(reader),
}
}
pub fn mime(mut self, content_type: &str) -> Self {
if let Ok(value) = http::HeaderValue::from_str(content_type) {
self.parts.headers.insert(http::header::CONTENT_TYPE, value);
}
self
}
pub fn status(mut self, code: u16) -> Self {
self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
self
}
pub fn header(mut self, name: &str, value: &str) -> Self {
if let (Ok(header_name), Ok(header_value)) = (
name.parse::<http::header::HeaderName>(),
http::HeaderValue::from_str(value),
) {
self.parts.headers.insert(header_name, header_value);
}
self
}
pub fn cors(self) -> Self {
self.header("access-control-allow-origin", "null")
}
}