v0.0.14 - Fix tab URL tracking: use main-frame URI instead of decide-policy, sync URL bar on navigation
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ members = [
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "sovereign_browser"
|
name = "sovereign_browser"
|
||||||
version = "0.0.13"
|
version = "0.0.14"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
|
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
|
||||||
|
|||||||
+5
-1
@@ -111,8 +111,12 @@ fn handle_sovereign_scheme(request: &URISchemeRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Processes REST API ─────────────────────────────────────────
|
// ── Processes REST API ─────────────────────────────────────────
|
||||||
|
// Pass `path` (with its query string): probe-report reads
|
||||||
|
// tab_index/body and tab_probe/tab_action read index/action from it.
|
||||||
|
// Passing `route` dropped every parameter, so no probe report was ever
|
||||||
|
// recorded and the Tabs view stayed blank.
|
||||||
if route.starts_with("processes/") {
|
if route.starts_with("processes/") {
|
||||||
handle_processes_api(request, &route[10..]);
|
handle_processes_api(request, &path[10..]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-3
@@ -229,15 +229,19 @@ fn add_scrollbar_visibility_fix(manager: &UserContentManager) {
|
|||||||
manager.add_style_sheet(&sheet);
|
manager.add_style_sheet(&sheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perf probe preamble — skips sovereign:// and about: pages, sets tab index.
|
/// Perf probe preamble — skips sovereign:// pages, sets tab index.
|
||||||
/// The tab index is baked in at injection time as a literal.
|
/// The tab index is baked in at injection time as a literal.
|
||||||
|
///
|
||||||
|
/// Only `sovereign:` is skipped. The previous check also skipped `about:`,
|
||||||
|
/// but at document-start the location of a freshly created top-level
|
||||||
|
/// document can still be `about:blank`, which silently disabled the probe
|
||||||
|
/// for every real page.
|
||||||
fn perf_probe_preamble(tab_index: i32) -> String {
|
fn perf_probe_preamble(tab_index: i32) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"(function(){{
|
r#"(function(){{
|
||||||
'use strict';
|
'use strict';
|
||||||
try {{
|
try {{
|
||||||
var u = location.href || '';
|
if (location.protocol === 'sovereign:') {{
|
||||||
if (u.indexOf('sovereign://') === 0 || u.indexOf('about:') === 0) {{
|
|
||||||
window.__sbPerfProbeSkip = true;
|
window.__sbPerfProbeSkip = true;
|
||||||
}} else {{
|
}} else {{
|
||||||
window.__sbPerfProbeSkip = false;
|
window.__sbPerfProbeSkip = false;
|
||||||
@@ -250,6 +254,28 @@ fn perf_probe_preamble(tab_index: i32) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Isolated-world relay for perf probe reports. The probe (main world)
|
||||||
|
/// dispatches a `__sb_probe_report` DOM event carrying the JSON payload as a
|
||||||
|
/// string; this relay performs the synchronous XHR to
|
||||||
|
/// `sovereign://processes/probe-report`. Doing the XHR from an isolated world
|
||||||
|
/// exempts it from the page's Content-Security-Policy (connect-src), which
|
||||||
|
/// otherwise blocks the request on many sites.
|
||||||
|
const PERF_PROBE_RELAY_JS: &str = r#"
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
document.addEventListener('__sb_probe_report', function(ev) {
|
||||||
|
try {
|
||||||
|
var d = JSON.parse(ev.detail);
|
||||||
|
var url = 'sovereign://processes/probe-report?tab_index=' + d.tab_index +
|
||||||
|
'&body=' + encodeURIComponent(ev.detail);
|
||||||
|
var x = new XMLHttpRequest();
|
||||||
|
x.open('GET', url, false);
|
||||||
|
x.send();
|
||||||
|
} catch (e) { /* probe must never break the page */ }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
"#;
|
||||||
|
|
||||||
/// The perf-probe.js script (embedded from www/js/perf-probe.js).
|
/// The perf-probe.js script (embedded from www/js/perf-probe.js).
|
||||||
const PERF_PROBE_JS: &str = include_str!("../www/js/perf-probe.js");
|
const PERF_PROBE_JS: &str = include_str!("../www/js/perf-probe.js");
|
||||||
|
|
||||||
@@ -274,6 +300,16 @@ pub fn nostr_inject_create_content_manager_with_probe(tab_index: i32) -> UserCon
|
|||||||
// Top frame only — mirrors the C version (perf_probe.c:67). Injecting
|
// Top frame only — mirrors the C version (perf_probe.c:67). Injecting
|
||||||
// into all frames would run a separate probe instance (with its own
|
// into all frames would run a separate probe instance (with its own
|
||||||
// rAF loop and 1s sync XHR) in every iframe.
|
// rAF loop and 1s sync XHR) in every iframe.
|
||||||
|
let probe_relay = UserScript::for_world(
|
||||||
|
PERF_PROBE_RELAY_JS,
|
||||||
|
UserContentInjectedFrames::TopFrame,
|
||||||
|
UserScriptInjectionTime::Start,
|
||||||
|
BRIDGE_WORLD,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
manager.add_script(&probe_relay);
|
||||||
|
|
||||||
let preamble = UserScript::new(
|
let preamble = UserScript::new(
|
||||||
&perf_probe_preamble(tab_index),
|
&perf_probe_preamble(tab_index),
|
||||||
UserContentInjectedFrames::TopFrame,
|
UserContentInjectedFrames::TopFrame,
|
||||||
|
|||||||
+45
-7
@@ -841,11 +841,13 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tab URL in state for non-rewritten URLs.
|
// NOTE: the tab's URL in state is intentionally NOT
|
||||||
let mut state = G_TAB_STATE.lock().unwrap();
|
// updated here. decide-policy also fires for
|
||||||
if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_clone2) {
|
// sub-frame/iframe navigations (e.g. a PayPal
|
||||||
t.url = uri_str;
|
// button) and for not-yet-committed requests, which
|
||||||
}
|
// would clobber the real page URL. The main-frame
|
||||||
|
// URI is tracked via `notify::uri` below.
|
||||||
|
let _ = tab_id_clone2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -853,6 +855,31 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti
|
|||||||
true
|
true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track the main-frame URI: keep the tab state and the URL entry in
|
||||||
|
// sync with what the webview is actually showing (covers link clicks,
|
||||||
|
// redirects, history navigation, and Recents/Bookmarks menu loads).
|
||||||
|
let tab_id_uri = tab_id;
|
||||||
|
let url_entry_uri = url_entry.clone();
|
||||||
|
webview.connect_uri_notify(move |wv| {
|
||||||
|
let Some(uri) = wv.uri() else { return };
|
||||||
|
let uri_str = uri.to_string();
|
||||||
|
if uri_str.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut state = G_TAB_STATE.lock().unwrap();
|
||||||
|
if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_uri) {
|
||||||
|
t.url = uri_str.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if uri_str.starts_with("about:blank") {
|
||||||
|
url_entry_uri.set_text("");
|
||||||
|
} else if url_entry_uri.text().as_str() != uri_str {
|
||||||
|
url_entry_uri.set_text(&uri_str);
|
||||||
|
url_entry_uri.set_position(-1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Webview context menu — add "Open Link in New Tab" when right-clicking
|
// Webview context menu — add "Open Link in New Tab" when right-clicking
|
||||||
// a link, and "Open Page in New Tab" when right-clicking the page
|
// a link, and "Open Page in New Tab" when right-clicking the page
|
||||||
// background. Mirrors the C version (tab_manager.c:1912).
|
// background. Mirrors the C version (tab_manager.c:1912).
|
||||||
@@ -1669,8 +1696,13 @@ pub fn tab_manager_reload_tab(tab_id: i32) {
|
|||||||
/// Duplicate a tab by ID (opens the same URL in a new tab in the same window).
|
/// Duplicate a tab by ID (opens the same URL in a new tab in the same window).
|
||||||
pub fn tab_manager_duplicate(tab_id: i32) {
|
pub fn tab_manager_duplicate(tab_id: i32) {
|
||||||
let state = G_TAB_STATE.lock().unwrap();
|
let state = G_TAB_STATE.lock().unwrap();
|
||||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||||
drop(state);
|
drop(state);
|
||||||
|
// Prefer the webview's live main-frame URI over the stored one.
|
||||||
|
let url = tab_webview(tab_id)
|
||||||
|
.and_then(|wv| wv.uri().map(|u| u.to_string()))
|
||||||
|
.filter(|u| !u.is_empty())
|
||||||
|
.or(state_url);
|
||||||
// Duplicate into the tab's own notebook (its window).
|
// Duplicate into the tab's own notebook (its window).
|
||||||
let nb = match tab_notebook(tab_id) {
|
let nb = match tab_notebook(tab_id) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
@@ -1692,7 +1724,13 @@ pub fn tab_manager_open_in_new_window(tab_id: i32) {
|
|||||||
// Capture the URL and the tab's own notebook before moving.
|
// Capture the URL and the tab's own notebook before moving.
|
||||||
let (url, nb) = {
|
let (url, nb) = {
|
||||||
let state = G_TAB_STATE.lock().unwrap();
|
let state = G_TAB_STATE.lock().unwrap();
|
||||||
let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone());
|
||||||
|
drop(state);
|
||||||
|
// Prefer the webview's live main-frame URI over the stored one.
|
||||||
|
let url = tab_webview(tab_id)
|
||||||
|
.and_then(|wv| wv.uri().map(|u| u.to_string()))
|
||||||
|
.filter(|u| !u.is_empty())
|
||||||
|
.or(state_url);
|
||||||
(url, tab_notebook(tab_id))
|
(url, tab_notebook(tab_id))
|
||||||
};
|
};
|
||||||
let nb = match nb {
|
let nb = match nb {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
//! Version information for sovereign_browser
|
//! Version information for sovereign_browser
|
||||||
|
|
||||||
/// The current version of sovereign_browser (with leading 'v').
|
/// The current version of sovereign_browser (with leading 'v').
|
||||||
pub const VERSION: &str = "v0.0.13";
|
pub const VERSION: &str = "v0.0.14";
|
||||||
|
|
||||||
/// Major version number.
|
/// Major version number.
|
||||||
pub const VERSION_MAJOR: u32 = 0;
|
pub const VERSION_MAJOR: u32 = 0;
|
||||||
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
|
|||||||
pub const VERSION_MINOR: u32 = 0;
|
pub const VERSION_MINOR: u32 = 0;
|
||||||
|
|
||||||
/// Patch version number.
|
/// Patch version number.
|
||||||
pub const VERSION_PATCH: u32 = 13;
|
pub const VERSION_PATCH: u32 = 14;
|
||||||
|
|||||||
+44
-13
@@ -125,6 +125,8 @@
|
|||||||
try {
|
try {
|
||||||
var resObs = new PerformanceObserver(function (list) {
|
var resObs = new PerformanceObserver(function (list) {
|
||||||
list.getEntries().forEach(function (e) {
|
list.getEntries().forEach(function (e) {
|
||||||
|
// Ignore the probe's own reports and other browser-internal calls.
|
||||||
|
if (e.name.indexOf('sovereign://') === 0) return;
|
||||||
netRequestsLastSec.push({ name: e.name, type: e.initiatorType });
|
netRequestsLastSec.push({ name: e.name, type: e.initiatorType });
|
||||||
var key = e.name.split('?')[0];
|
var key = e.name.split('?')[0];
|
||||||
netEndpoints[key] = (netEndpoints[key] || 0) + 1;
|
netEndpoints[key] = (netEndpoints[key] || 0) + 1;
|
||||||
@@ -135,13 +137,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── FPS via requestAnimationFrame ───────────────────────────────
|
// ── FPS via requestAnimationFrame ───────────────────────────────
|
||||||
|
// The rAF loop is the probe's only continuous main-thread work. It is
|
||||||
|
// paused whenever the document is hidden (background tab / minimised
|
||||||
|
// window) so a backgrounded tab can never keep the compositor and font
|
||||||
|
// machinery busy. It is also guarded so callbacks can't stack.
|
||||||
var rafFrames = 0;
|
var rafFrames = 0;
|
||||||
var fps = 0;
|
var fps = 0;
|
||||||
function rafLoop(ts) {
|
var rafScheduled = false;
|
||||||
|
var rafActive = false;
|
||||||
|
|
||||||
|
function rafLoop() {
|
||||||
|
rafScheduled = false;
|
||||||
|
if (!rafActive) return;
|
||||||
rafFrames++;
|
rafFrames++;
|
||||||
|
scheduleRaf();
|
||||||
|
}
|
||||||
|
function scheduleRaf() {
|
||||||
|
if (rafScheduled || !rafActive) return;
|
||||||
|
rafScheduled = true;
|
||||||
requestAnimationFrame(rafLoop);
|
requestAnimationFrame(rafLoop);
|
||||||
}
|
}
|
||||||
requestAnimationFrame(rafLoop);
|
function setRafActive(on) {
|
||||||
|
if (on === rafActive) return;
|
||||||
|
rafActive = on;
|
||||||
|
if (on) {
|
||||||
|
scheduleRaf();
|
||||||
|
} else {
|
||||||
|
rafFrames = 0;
|
||||||
|
fps = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rafActive = !document.hidden;
|
||||||
|
if (rafActive) scheduleRaf();
|
||||||
|
|
||||||
|
var onVisibility = function () { setRafActive(!document.hidden); };
|
||||||
|
onVisibility.__sbInternal = true; // don't count our own listener
|
||||||
|
document.addEventListener('visibilitychange', onVisibility);
|
||||||
|
|
||||||
// ── Timer tracking (patched setTimeout/setInterval) ─────────────
|
// ── Timer tracking (patched setTimeout/setInterval) ─────────────
|
||||||
var activeTimers = {}; // id -> {code, interval_ms, count}
|
var activeTimers = {}; // id -> {code, interval_ms, count}
|
||||||
@@ -344,21 +375,21 @@
|
|||||||
netEndpoints = {};
|
netEndpoints = {};
|
||||||
timerTop = {};
|
timerTop = {};
|
||||||
|
|
||||||
// Send. Use sync XHR to sovereign:// (same convention as the
|
// Send. The actual sync XHR to sovereign:// runs in an isolated
|
||||||
// window.nostr shim — WebKit's custom scheme handler doesn't
|
// script world (see PERF_PROBE_RELAY_JS in nostr_inject.rs) so that
|
||||||
// reliably support fetch() or async XHR for sovereign:// URLs).
|
// pages with a strict CSP can't block it. We hand the payload over as
|
||||||
|
// a JSON string in a DOM CustomEvent (objects don't cross worlds).
|
||||||
try {
|
try {
|
||||||
var body = encodeURIComponent(JSON.stringify(payload));
|
document.dispatchEvent(new CustomEvent('__sb_probe_report', {
|
||||||
var url = 'sovereign://processes/probe-report?tab_index=' + TAB_INDEX +
|
detail: JSON.stringify(payload)
|
||||||
'&body=' + body;
|
}));
|
||||||
var x = new XMLHttpRequest();
|
|
||||||
x.open('GET', url, false); // synchronous
|
|
||||||
x.send();
|
|
||||||
} catch (e) { /* swallow — probe must never break the page */ }
|
} catch (e) { /* swallow — probe must never break the page */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// FPS counter: count frames in each 1s window.
|
// FPS counter: count frames in each 1s window. Use the original
|
||||||
setInterval(function () {
|
// setInterval so the probe doesn't count itself as an active timer.
|
||||||
|
origSetInterval.call(window, function () {
|
||||||
|
if (!rafActive) return;
|
||||||
fps = rafFrames;
|
fps = rafFrames;
|
||||||
rafFrames = 0;
|
rafFrames = 0;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
Reference in New Issue
Block a user