use crate::BUILTIN_BROWSER_APPID;
use crate::chooser::browser_choose_files;
use crate::document_session::{BrowserDocumentSessions, browser_document_sessions};
use crate::downloads::browser_download_resource;
use crate::inbound::{
BorrowedBrowserEnvelope, BrowserInboundDiagnostics, BrowserInboundRejectReason,
BrowserV3InboundKind, ConsoleRateLimiter, parse_browser_envelope,
};
use crate::internal_pages::{
LingxiaSchemeContext, browser_attach_tab_page, browser_document_scripts_snapshot,
browser_load_internal_document, browser_resolve_delegate_context,
browser_resolve_delegate_page, ensure_browser_startup_page, handle_browser_lingxia_scheme,
};
use crate::policy::{
BrowserNavigationPolicySession, LINGXIA_SCHEME, extract_url_scheme,
normalize_browser_target_url,
};
use crate::tabs::{
TabCreateState, browser_clear_pending_if_token_matches,
browser_commit_navigation_if_token_matches, browser_internal_url_if_token_matches,
browser_remove_tab_if_token_matches, browser_tab_create_state, browser_tab_generation_matches,
ensure_browser_lxapp,
};
use crate::types::{BrowserNavigationPolicyDecision, BrowserNavigationPolicyRequest};
use lingxia_log::{LogBuilder, LogLevel as LxLogLevel, LogTag};
use lingxia_platform::traits::app_runtime::{AppRuntime, OpenUrlRequest, OpenUrlTarget};
use lingxia_webview::runtime::{
destroy_webview_if_matches as destroy_managed_webview_if_matches,
find_webview as find_managed_webview,
};
use lingxia_webview::{
IncomingWebMessage, LoadDataRequest, LoadError, LoadErrorPage, LogLevel, NativeWebViewId,
NavigationEvent, NavigationId, NavigationPolicy, NavigationProgress, NewWindowPolicy,
TrustedDocumentAdmission, UserAgentOverride, WebTag, WebView, WebViewBuilder,
WebViewController, WebViewDataMode, WebViewDelegate, WebViewSession, WebViewStateChange,
render_load_error_page,
};
use lxapp::LxAppError;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct BrowserTabDelegate {
tab_id: String,
page_path: String,
session_id: u64,
create_token: u64,
data_mode: WebViewDataMode,
url_callback: Arc<AtomicBool>,
standalone: bool,
documents: Arc<BrowserDocumentSessions>,
native_view: std::sync::Mutex<Option<NativeWebViewId>>,
navigation: std::sync::Mutex<BrowserTabNavigationState>,
inbound_diagnostics: BrowserInboundDiagnostics,
console_rate_limiter: std::sync::Mutex<ConsoleRateLimiter>,
}
#[derive(Default)]
struct BrowserTabNavigationState {
progress: NavigationProgress,
showing_error_page: bool,
current_attempt_trusted: bool,
}
impl BrowserTabNavigationState {
fn record_start_trust(&mut self, trusted: bool) {
self.current_attempt_trusted = trusted;
}
fn restored_internal_url<'a>(
&self,
navigation_id: NavigationId,
final_url: &'a str,
) -> Option<&'a str> {
(self.progress.is_current(navigation_id)
&& !self.current_attempt_trusted
&& extract_url_scheme(final_url).as_deref() == Some(LINGXIA_SCHEME))
.then_some(final_url)
}
}
const BROWSER_LOAD_ERROR_URL: &str = "lingxia://browser/load-error";
fn is_error_document_url(url: &str) -> bool {
url.trim_end_matches('/')
.eq_ignore_ascii_case(BROWSER_LOAD_ERROR_URL)
}
impl BrowserTabDelegate {
fn bind_native_view(&self, native_view: NativeWebViewId) {
if let Ok(mut slot) = self.native_view.lock() {
*slot = Some(native_view);
}
}
fn native_view(&self) -> Option<NativeWebViewId> {
self.native_view.lock().ok().and_then(|slot| *slot)
}
fn records_browser_history(&self) -> bool {
self.data_mode != WebViewDataMode::Ephemeral
&& !self.url_callback.load(Ordering::Acquire)
&& !self.standalone
}
fn revoke_document_authority(&self, authority: lxapp::ControlDocumentAuthority) {
match browser_resolve_delegate_page(&self.page_path, self.session_id) {
Ok(page) => {
let _ =
page.revoke_required_v3_document(self.documents.native_authority(), authority);
}
Err(err) => {
lxapp::warn!(
"[InternalBrowser] Failed to cancel revoked bridge session for tab {}: {}",
self.tab_id,
err
);
}
}
}
fn inject_document_scripts(&self) {
let scripts = browser_document_scripts_snapshot();
if scripts.is_empty() {
return;
}
let Ok(webview) = browser_find_webview(&self.page_path, self.session_id) else {
return;
};
for js in &scripts {
if let Err(err) = webview.exec_js(js) {
lxapp::warn!(
"[InternalBrowser] document script injection failed for tab {}: {}",
self.tab_id,
err
);
}
}
}
fn show_load_error(&self, error: &LoadError) {
crate::internal_pages::detach_internal_tab_page(&self.page_path);
let failing_url = error.failing_url.as_deref().unwrap_or_default();
let title =
lingxia_platform::i18n::text("webview.load_error_title", "Couldn't load this page");
let _ = crate::tabs::browser_update_tab_info_if_token_matches(
&self.tab_id,
self.session_id,
self.create_token,
(!failing_url.is_empty()).then_some(failing_url),
Some(&title),
);
let Ok(webview) = browser_find_webview(&self.page_path, self.session_id) else {
return;
};
let message = lingxia_platform::i18n::text(
"webview.load_error_message",
"Check your connection and try again.",
);
let retry = lingxia_platform::i18n::text("webview.retry", "Retry");
let html = render_load_error_page(LoadErrorPage {
title: &title,
message: &message,
retry_label: &retry,
retry_url: failing_url,
});
if let Err(err) = webview.load_data(
LoadDataRequest::new(&html, BROWSER_LOAD_ERROR_URL)
.with_history_url(BROWSER_LOAD_ERROR_URL),
) {
if let Ok(mut navigation) = self.navigation.lock() {
navigation.showing_error_page = false;
}
lxapp::warn!(
"[InternalBrowser] Failed to render load error for tab {}: {}",
self.tab_id,
err
);
}
}
fn schedule_trusted_internal_reload(&self, url: String, reason: &'static str) {
let tab_id = self.tab_id.clone();
let page_path = self.page_path.clone();
let session_id = self.session_id;
let create_token = self.create_token;
let native_view = self.native_view();
let documents = Arc::clone(&self.documents);
rong::RongExecutor::global().spawn(async move {
if !browser_tab_generation_matches(&tab_id, session_id, create_token) {
return;
}
let Ok(webview) = browser_find_webview(&page_path, session_id) else {
return;
};
if native_view != Some(webview.native_view_id())
|| !browser_tab_generation_matches(&tab_id, session_id, create_token)
{
return;
}
if let Err(error) = browser_load_internal_document(
webview,
&documents,
&page_path,
session_id,
create_token,
&url,
) {
lxapp::warn!(
"[InternalBrowser] Failed to replace {reason} document for tab {tab_id}: {error}"
);
}
});
}
}
impl WebViewDelegate for BrowserTabDelegate {
fn on_navigation_event(&self, event: NavigationEvent) {
let mut navigation = self.navigation.lock().unwrap_or_else(|e| e.into_inner());
navigation.progress.apply(&event);
match &event {
NavigationEvent::Started { id, requested_url } => {
let (trusted_start, revoked) = self
.native_view()
.map(|native_view| {
self.documents
.navigation_started_with_revocation(native_view, *id)
})
.unwrap_or((false, None));
navigation.record_start_trust(trusted_start);
if is_error_document_url(requested_url) {
navigation.showing_error_page = true;
drop(navigation);
if let Some(authority) = revoked {
self.revoke_document_authority(authority);
}
return;
}
navigation.showing_error_page = false;
drop(navigation);
if let Some(authority) = revoked {
self.revoke_document_authority(authority);
}
if !trusted_start {
return;
}
match browser_resolve_delegate_page(&self.page_path, self.session_id) {
Ok(page) => page.notify_page_started(),
Err(err) => {
lxapp::warn!(
"[InternalBrowser] Failed to resolve delegate page for tab {} on start: {}",
self.tab_id,
err
);
}
}
}
NavigationEvent::Succeeded { id, final_url } => {
if is_error_document_url(final_url) {
return;
}
let restored_internal = navigation.restored_internal_url(*id, final_url).is_some();
let is_current = navigation.progress.is_current(*id);
drop(navigation);
let internal = extract_url_scheme(final_url).as_deref() == Some(LINGXIA_SCHEME);
if restored_internal {
crate::internal_pages::detach_internal_tab_page(&self.page_path);
self.schedule_trusted_internal_reload(final_url.clone(), "restored internal");
return;
}
self.inject_document_scripts();
if !internal {
crate::internal_pages::detach_internal_tab_page(&self.page_path);
}
if self.records_browser_history() {
crate::tabs::notify_navigation_finished(
&self.tab_id,
self.session_id,
self.create_token,
final_url,
);
}
if internal && is_current {
match browser_resolve_delegate_page(&self.page_path, self.session_id) {
Ok(page) => page.handle_loaded(),
Err(err) => {
lxapp::warn!(
"[InternalBrowser] Failed to resolve delegate page for tab {} on finish: {}",
self.tab_id,
err
);
}
}
}
}
NavigationEvent::Failed { id, error } => {
let revoked = self.native_view().and_then(|native_view| {
self.documents
.revoke_navigation_if_matches(native_view, *id)
});
let failing_url = error.failing_url.as_deref().unwrap_or_default();
if is_error_document_url(failing_url) || navigation.showing_error_page {
navigation.showing_error_page = false;
drop(navigation);
if let Some(authority) = revoked {
self.revoke_document_authority(authority);
}
return;
}
if !navigation.progress.is_current(*id) {
drop(navigation);
if let Some(authority) = revoked {
self.revoke_document_authority(authority);
}
return;
}
navigation.showing_error_page = true;
drop(navigation);
if let Some(authority) = revoked {
self.revoke_document_authority(authority);
}
self.show_load_error(error);
}
NavigationEvent::Cancelled { id, .. } => {
drop(navigation);
if let Some(authority) = self.native_view().and_then(|native_view| {
self.documents
.revoke_navigation_if_matches(native_view, *id)
}) {
self.revoke_document_authority(authority);
}
}
}
}
fn on_trusted_document_admitted(&self, admission: TrustedDocumentAdmission) {
if self.native_view() != Some(admission.native_view()) {
return;
}
let _ = self
.documents
.admit(self.session_id, self.create_token, admission);
}
fn on_web_content_process_terminated(&self, native_view: NativeWebViewId) {
if self.native_view() != Some(native_view) {
return;
}
if let Some(authority) = self.documents.destroy_native_view(native_view) {
self.revoke_document_authority(authority);
}
if let Some(url) =
browser_internal_url_if_token_matches(&self.tab_id, self.session_id, self.create_token)
{
self.schedule_trusted_internal_reload(url, "terminated renderer");
}
}
fn on_document_restored(&self, native_view: NativeWebViewId, url: &str) {
if self.native_view() != Some(native_view) {
return;
}
if let Some(authority) = self.documents.destroy_native_view(native_view) {
self.revoke_document_authority(authority);
}
crate::internal_pages::detach_internal_tab_page(&self.page_path);
if extract_url_scheme(url).as_deref() == Some(LINGXIA_SCHEME) {
self.schedule_trusted_internal_reload(url.to_owned(), "restored document");
}
}
fn on_webview_state_change(&self, change: WebViewStateChange) {
if self
.navigation
.lock()
.unwrap_or_else(|e| e.into_inner())
.showing_error_page
&& matches!(
&change,
WebViewStateChange::Location { .. } | WebViewStateChange::Title { .. }
)
{
return;
}
match change {
WebViewStateChange::Location { url } => {
let _ = crate::tabs::browser_update_tab_info_if_token_matches(
&self.tab_id,
self.session_id,
self.create_token,
Some(&url),
None,
);
}
WebViewStateChange::Title { title } => {
if let Some(title) = title {
let _ = crate::tabs::browser_update_tab_info_if_token_matches(
&self.tab_id,
self.session_id,
self.create_token,
None,
Some(&title),
);
}
}
WebViewStateChange::Favicon { png_bytes } => {
if !crate::tabs::browser_tab_generation_matches(
&self.tab_id,
self.session_id,
self.create_token,
) {
return;
}
let _ = crate::tabs::browser_update_tab_favicon(
&self.tab_id,
png_bytes.unwrap_or_default(),
);
}
WebViewStateChange::BackForwardAvailability {
can_go_back,
can_go_forward,
} => {
let _ = crate::tabs::browser_update_tab_nav_state(
&self.tab_id,
can_go_back,
can_go_forward,
);
}
}
}
fn handle_post_message(&self, message: IncomingWebMessage) {
let envelope = match parse_browser_envelope(message.body()) {
Ok(envelope) => envelope,
Err(reason) => {
self.inbound_diagnostics.reject(reason);
return;
}
};
let context = message.context().clone();
if let BorrowedBrowserEnvelope::Console {
binding,
level,
message,
} = envelope
{
let result = self.documents.with_active_bound_context(
&context,
binding.session_id,
binding.secret,
|_| {
let Ok(mut limiter) = self.console_rate_limiter.lock() else {
self.inbound_diagnostics
.reject(BrowserInboundRejectReason::SessionNotReady);
return None;
};
if !limiter.allow() {
self.inbound_diagnostics
.reject(BrowserInboundRejectReason::ConsoleRateLimited);
return None;
}
self.log(level, &message);
Some(())
},
);
if let Err(reason) = result {
self.inbound_diagnostics.reject(reason);
}
return;
}
let BorrowedBrowserEnvelope::Bridge { kind, binding } = envelope else {
unreachable!("console envelope returned above");
};
let is_hello = kind == BrowserV3InboundKind::Hello;
let session_id = binding.session_id.to_owned();
let secret = binding.secret.to_owned();
match browser_resolve_delegate_page(&self.page_path, self.session_id) {
Ok(page) => {
let mut deferred_cancellation = None;
let typed_rejected = std::cell::Cell::new(false);
let prepared = if is_hello {
self.documents.with_activated_hello_for_context(
&context,
&session_id,
&secret,
|authority, gate| {
let Ok(deferred) = page.bind_required_v3_authority(
self.documents.native_authority(),
&context,
authority,
gate,
) else {
return false;
};
deferred_cancellation = Some(deferred);
true
},
|authority| {
page.promote_active_browser_document(
self.documents.native_authority(),
authority.clone(),
)
.then(|| {
let prepared = page.prepare_required_v3_incoming(
self.documents.native_authority(),
message,
authority.clone(),
authority.execution_gate(),
);
if prepared.is_err() {
typed_rejected.set(true);
}
prepared.ok()
})
.flatten()
},
)
} else {
self.documents.with_active_frame_for_context(
&context,
&session_id,
&secret,
|authority| {
let prepared = page.prepare_required_v3_incoming(
self.documents.native_authority(),
message,
authority.clone(),
authority.execution_gate(),
);
if prepared.is_err() {
typed_rejected.set(true);
}
prepared.ok()
},
)
};
let prepared = match prepared {
Ok(prepared) => prepared,
Err(reason) => {
self.inbound_diagnostics.reject(reason);
return;
}
};
if typed_rejected.get() {
self.inbound_diagnostics
.reject(BrowserInboundRejectReason::MalformedEnvelope);
}
if let Some(deferred) = deferred_cancellation {
deferred.finish();
}
if let Some(prepared) = prepared
&& page
.execute_prepared_required_v3_incoming(prepared)
.is_err()
{
self.inbound_diagnostics
.reject(BrowserInboundRejectReason::DispatchFailed);
}
}
Err(_) => {
self.inbound_diagnostics
.reject(BrowserInboundRejectReason::SessionNotReady);
}
}
}
fn log(&self, level: LogLevel, message: &str) {
let log_level = match level {
LogLevel::Error => LxLogLevel::Error,
LogLevel::Warn => LxLogLevel::Warn,
LogLevel::Info => LxLogLevel::Info,
LogLevel::Debug | LogLevel::Verbose => LxLogLevel::Debug,
};
LogBuilder::new(LogTag::BrowserConsole, message)
.with_level(log_level)
.with_path(&self.page_path)
.with_appid(BUILTIN_BROWSER_APPID.to_string());
}
}
fn browser_webtag(path: &str, session_id: u64) -> WebTag {
WebTag::new(BUILTIN_BROWSER_APPID, path, Some(session_id))
}
fn callback_blocks_file_navigation(url_callback: bool, url: &str) -> bool {
url_callback && extract_url_scheme(url).as_deref() == Some("file")
}
fn callback_policy_blocks_file_navigation(url_callback: &AtomicBool, url: &str) -> bool {
callback_blocks_file_navigation(url_callback.load(Ordering::Acquire), url)
}
fn url_callback_opens_new_window_externally(url_callback: bool) -> bool {
url_callback
}
pub(crate) fn browser_create_webview(
path: &str,
session_id: u64,
tab_id: &str,
create_token: u64,
data_mode: WebViewDataMode,
url_callback: Arc<AtomicBool>,
standalone: bool,
) -> Result<(), LxAppError> {
let webtag = browser_webtag(path, session_id);
let browser_owner = ensure_browser_lxapp()?;
let tab_path_owned = path.to_string();
let tab_id_owned = tab_id.to_string();
ensure_browser_startup_page(&browser_owner)?;
let tab_id_for_lx = tab_id_owned.clone();
let tab_path_for_lx = tab_path_owned.clone();
let documents = browser_document_sessions();
let lingxia_ctx = Arc::new(LingxiaSchemeContext {
browser: browser_owner.clone(),
startup_path: browser_owner.initial_route(),
tab_id: tab_id_owned.clone(),
tab_path: tab_path_owned.clone(),
session_id,
});
let runtime_for_nav = browser_owner.runtime.clone();
let owner_appid_for_nav = browser_owner.appid.clone();
let owner_session_for_nav = browser_owner.session_id();
let runtime_for_new_window = browser_owner.runtime.clone();
let owner_appid_for_new_window = browser_owner.appid.clone();
let owner_session_for_new_window = browser_owner.session_id();
let tab_id_for_new_window = tab_id_owned.clone();
let tab_id_for_download = tab_id_owned.clone();
let owner_for_download = browser_owner.clone();
let owner_for_file_chooser = browser_owner.clone();
let url_callback_for_navigation = url_callback.clone();
let policy_session_for_navigation = Arc::new(std::sync::Mutex::new(
BrowserNavigationPolicySession::default(),
));
let delegate = Arc::new(BrowserTabDelegate {
tab_id: tab_id_owned.clone(),
page_path: tab_path_owned.clone(),
session_id,
create_token,
data_mode,
url_callback: url_callback.clone(),
standalone,
documents: documents.clone(),
native_view: std::sync::Mutex::new(None),
navigation: std::sync::Mutex::new(BrowserTabNavigationState::default()),
inbound_diagnostics: BrowserInboundDiagnostics::default(),
console_rate_limiter: std::sync::Mutex::new(ConsoleRateLimiter::default()),
});
let session = WebViewBuilder::browser(webtag)
.data_mode(data_mode)
.delegate(delegate.clone())
.on_scheme("lx", move |req| {
let tab_id = tab_id_for_lx.clone();
let tab_path = tab_path_for_lx.clone();
async move {
match browser_resolve_delegate_context(&tab_path, session_id) {
Ok((owner, page)) => owner.handle_lingxia_request(&page, req).into(),
Err(err) => {
lxapp::warn!(
"[InternalBrowser] Failed to resolve lx:// owner for tab {}: {}",
tab_id,
err
);
None.into()
}
}
}
})
.on_scheme(LINGXIA_SCHEME, move |req| {
let ctx = lingxia_ctx.clone();
async move { handle_browser_lingxia_scheme(&ctx, req).await.into() }
})
.on_navigation(move |request| {
let url = request.url.as_str();
if callback_policy_blocks_file_navigation(&url_callback_for_navigation, url) {
return NavigationPolicy::Cancel;
}
let policy_request = BrowserNavigationPolicyRequest {
raw_url: url.to_string(),
has_user_gesture: request.has_user_gesture,
is_main_frame: request.is_main_frame,
};
let evaluation = match policy_session_for_navigation.lock() {
Ok(mut session) => session.evaluate(policy_request),
Err(_) => {
lxapp::warn!("[InternalBrowser] Navigation policy session poisoned");
return NavigationPolicy::Cancel;
}
};
if evaluation.inherited_user_activation {
lxapp::debug!(
"[InternalBrowser] Inherited transient user activation url={}",
url
);
}
let decision = evaluation.response;
match decision.decision {
BrowserNavigationPolicyDecision::InWebview => NavigationPolicy::Allow,
BrowserNavigationPolicyDecision::OpenExternal => {
let _ = runtime_for_nav.open_url(OpenUrlRequest {
owner_appid: owner_appid_for_nav.clone(),
owner_session_id: owner_session_for_nav,
url: url.to_string(),
target: OpenUrlTarget::External,
want_tab_id: false,
});
NavigationPolicy::Cancel
}
BrowserNavigationPolicyDecision::Deny => {
lxapp::debug!(
"[InternalBrowser] Denied navigation url={} reason={}",
url,
decision.reason.as_deref().unwrap_or("unspecified")
);
NavigationPolicy::Cancel
}
}
})
.on_new_window(move |url| {
if callback_policy_blocks_file_navigation(&url_callback, url) {
return NewWindowPolicy::Cancel;
}
let normalized = normalize_browser_target_url(url);
if url_callback_opens_new_window_externally(url_callback.load(Ordering::Acquire)) {
let _ = runtime_for_new_window.open_url(OpenUrlRequest {
owner_appid: owner_appid_for_new_window.clone(),
owner_session_id: owner_session_for_new_window,
url: normalized,
target: OpenUrlTarget::External,
want_tab_id: false,
});
return NewWindowPolicy::Cancel;
}
if crate::tabs::is_standalone_tab(&tab_id_for_new_window) {
let owner_appid = crate::tabs::tab_owner_appid(&tab_id_for_new_window);
rong::RongExecutor::global().spawn(async move {
let Some(owner) = owner_appid.as_deref().and_then(lxapp::try_get) else {
return;
};
static NEW_WINDOW_SEQ: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
let request = lxapp::PageSurfaceRequest::new(
format!(
"surface-aside-newwin-{}",
NEW_WINDOW_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
),
lxapp::PageSurfaceTarget::Url(normalized),
)
.position(lxapp::SurfacePosition::Right)
.role(lxapp::lingxia_surface::Role::Aside);
let _ = owner.open_surface(request);
});
return NewWindowPolicy::Cancel;
}
let target = if crate::tabs::is_aside_tab(&tab_id_for_new_window) {
OpenUrlTarget::AsideBrowser
} else {
OpenUrlTarget::NewBrowserTab
};
let _ = runtime_for_new_window.open_url(OpenUrlRequest {
owner_appid: owner_appid_for_new_window.clone(),
owner_session_id: owner_session_for_new_window,
url: normalized,
target,
want_tab_id: false,
});
NewWindowPolicy::Cancel
})
.on_download(move |request| {
let tab_id = tab_id_for_download.clone();
let owner = owner_for_download.clone();
rong::RongExecutor::global().spawn(async move {
browser_download_resource(owner, tab_id, request).await;
});
})
.on_file_chooser(move |request| {
let owner = owner_for_file_chooser.clone();
async move { browser_choose_files(owner, request).await }
})
.create();
rong::RongExecutor::global().spawn(async move {
browser_on_webview_ready(
tab_path_owned,
session_id,
tab_id_owned,
create_token,
session,
delegate,
documents,
)
.await;
});
Ok(())
}
async fn browser_on_webview_ready(
path: String,
session_id: u64,
tab_id: String,
create_token: u64,
session: WebViewSession,
delegate: Arc<BrowserTabDelegate>,
documents: Arc<BrowserDocumentSessions>,
) {
let webview = match session.wait_ready().await {
Ok(webview) => webview,
Err(e) => {
lxapp::warn!(
"[InternalBrowser] Failed to create webview for tab {}: {}",
tab_id,
e
);
browser_remove_tab_if_token_matches(&tab_id, session_id, create_token);
return;
}
};
delegate.bind_native_view(webview.native_view_id());
let tab_state = browser_tab_create_state(&tab_id, session_id, create_token);
match tab_state {
TabCreateState::Missing => {
browser_destroy_webview_if_matches(&path, session_id, &webview);
}
TabCreateState::Stale => {
browser_destroy_webview_if_matches(&path, session_id, &webview);
}
TabCreateState::Active {
pending_url,
user_agent_override,
} => {
if let Some(user_agent) = user_agent_override
&& let Err(e) =
webview.set_user_agent_override(UserAgentOverride::Custom(user_agent))
{
lxapp::warn!(
"[InternalBrowser] Failed to restore user agent for tab {}: {}",
tab_id,
e
);
}
if let Some(url) = pending_url {
let is_browser_internal =
extract_url_scheme(&url).as_deref() == Some(LINGXIA_SCHEME);
if is_browser_internal {
if let Err(e) = browser_attach_tab_page(
webview.clone(),
&documents,
&path,
session_id,
create_token,
&tab_id,
Some(url.as_str()),
)
.await
{
lxapp::warn!(
"[InternalBrowser] Failed to attach startup page for internal tab {}: {}",
tab_id,
e
);
browser_clear_pending_if_token_matches(&tab_id, session_id, create_token);
let _ = webview.load_url("about:blank");
} else {
browser_commit_navigation_if_token_matches(
&tab_id,
session_id,
create_token,
Some(&url),
);
}
} else {
if let Err(e) = webview.load_url(&url) {
lxapp::warn!(
"[InternalBrowser] Failed to load URL for tab {}: {}",
tab_id,
e
);
browser_clear_pending_if_token_matches(&tab_id, session_id, create_token);
} else {
browser_commit_navigation_if_token_matches(
&tab_id,
session_id,
create_token,
Some(&url),
);
}
}
} else {
if let Err(e) = browser_attach_tab_page(
webview.clone(),
&documents,
&path,
session_id,
create_token,
&tab_id,
None,
)
.await
{
lxapp::warn!(
"[InternalBrowser] Failed to load startup page for tab {}: {}",
tab_id,
e
);
let _ = webview.load_url("about:blank");
} else {
let startup_url = format!("{}://newtab", LINGXIA_SCHEME);
browser_commit_navigation_if_token_matches(
&tab_id,
session_id,
create_token,
Some(&startup_url),
);
}
}
}
}
}
pub(crate) fn browser_find_webview(
path: &str,
session_id: u64,
) -> Result<Arc<WebView>, LxAppError> {
let webtag = browser_webtag(path, session_id);
find_managed_webview(&webtag).ok_or_else(|| {
LxAppError::ResourceNotFound(format!("browser webview not found: {}", webtag.as_str()))
})
}
pub(crate) fn browser_load_url(
path: &str,
session_id: u64,
create_token: u64,
url: &str,
) -> Result<(), LxAppError> {
let webview = browser_find_webview(path, session_id)?;
if extract_url_scheme(url).as_deref() == Some(LINGXIA_SCHEME) {
return browser_load_internal_document(
webview,
&browser_document_sessions(),
path,
session_id,
create_token,
url,
);
}
webview.load_url(url).map_err(LxAppError::from)
}
pub(crate) fn browser_reload_document(
tab_id: &str,
path: &str,
session_id: u64,
create_token: u64,
current_url: Option<&str>,
) -> Result<(), LxAppError> {
if !browser_tab_generation_matches(tab_id, session_id, create_token) {
return Err(LxAppError::ResourceNotFound(format!(
"browser tab generation is stale: {tab_id}"
)));
}
if current_url.and_then(extract_url_scheme).as_deref() == Some(LINGXIA_SCHEME) {
return browser_load_url(
path,
session_id,
create_token,
current_url.expect("internal current URL was present"),
);
}
browser_find_webview(path, session_id)?
.reload()
.map_err(LxAppError::from)
}
pub(crate) fn browser_destroy_webview_if_matches(
path: &str,
session_id: u64,
expected: &Arc<WebView>,
) -> bool {
let webtag = browser_webtag(path, session_id);
let documents = browser_document_sessions();
if let Some(authority) = documents.destroy_native_view(expected.native_view_id())
&& let Ok(page) = browser_resolve_delegate_page(path, session_id)
{
let _ = page.revoke_required_v3_document(documents.native_authority(), authority);
}
destroy_managed_webview_if_matches(&webtag, expected)
}
#[cfg(test)]
mod tests {
use super::{
BrowserTabNavigationState, callback_blocks_file_navigation,
callback_policy_blocks_file_navigation, url_callback_opens_new_window_externally,
};
use lingxia_webview::{NavigationEvent, NavigationId};
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn login_callback_tabs_open_new_windows_in_the_system_browser() {
assert!(url_callback_opens_new_window_externally(true));
assert!(!url_callback_opens_new_window_externally(false));
}
#[test]
fn callback_tabs_block_file_navigation_only() {
assert!(callback_blocks_file_navigation(
true,
"file:///tmp/auth.html"
));
assert!(callback_blocks_file_navigation(
true,
"FILE:///tmp/auth.html"
));
assert!(!callback_blocks_file_navigation(
true,
"https://auth.example.com/callback"
));
assert!(!callback_blocks_file_navigation(
false,
"file:///tmp/document.html"
));
}
#[test]
fn callback_policy_updates_apply_to_an_existing_webview_handler() {
let policy = AtomicBool::new(false);
assert!(!callback_policy_blocks_file_navigation(
&policy,
"file:///tmp/document.html"
));
policy.store(true, Ordering::Release);
assert!(callback_policy_blocks_file_navigation(
&policy,
"file:///tmp/document.html"
));
}
#[test]
fn same_url_bfcache_restore_requires_a_fresh_trusted_load() {
let navigation_id = NavigationId::from_raw(41);
let mut state = BrowserTabNavigationState::default();
state.progress.apply(&NavigationEvent::Started {
id: navigation_id,
requested_url: "lingxia://settings".into(),
});
state.record_start_trust(false);
assert_eq!(
state.restored_internal_url(navigation_id, "lingxia://settings"),
Some("lingxia://settings")
);
assert_eq!(
state.restored_internal_url(navigation_id, "https://example.test/"),
None
);
}
#[test]
fn host_issued_internal_navigation_does_not_loop_through_restore_recovery() {
let navigation_id = NavigationId::from_raw(42);
let mut state = BrowserTabNavigationState::default();
state.progress.apply(&NavigationEvent::Started {
id: navigation_id,
requested_url: "lingxia://settings".into(),
});
state.record_start_trust(true);
assert_eq!(
state.restored_internal_url(navigation_id, "lingxia://settings"),
None
);
}
}