Add the Browser tab: a live web page with no browser chrome
A new tab (globe, after Maps) shows one web page full-bleed in the content area — no address bar, bookmarks or scrollbars — built for It Is Written but usable with any page. It loads at startup and keeps running in the background. Stage (browser-tab.js, browser-guest-preload.cjs): - An Electron <webview> per Browser tab, in its own sandboxed process and persistent session. The pages live in one persistent layer inside .tab-content: every tab switch rebuilds .tab-content and moving a webview reloads it, so app.js now clears around that layer (clearTabContentKeepingBrowser). - Tapping a text field brings up the Prayer tab's keyboard (createPrayerKeyboard with a sink) and types into the page with trusted input; Enter submits; the keyboard puts itself away when the page moves focus on (e.g. after a search). The OS touch keyboard is suppressed with inputmode="none" while a field is focused. - Tapping a scripture reference opens it in a new Bible tab, in the translation printed beside it when that one is switched on. References are found with the app's own scanner: scripture-scan.js gains scanAll() (every match with its offsets), translationAfter() and isTranslation(); scanText/parseSlide are unchanged. - Input inside a webview never reaches the app, so the page reports activity (idle splash) and five-finger touches (blank the screen) itself. - main.cjs: webviewTag on; every webview is forced sandboxed, context-isolated, no Node, http(s) only, with the guest preload. No new windows (opened in place), no permission prompts, no downloads, no pinch-zoom, no alert/confirm/prompt boxes, and "Leave site?" can never block Reload or Start page. The session reports plain Chrome. - server.js CSP: frame-src allows http(s) (a webview is a frame to CSP); style-src and font-src allow https (the follower's srcdoc copy inherits this policy). Everywhere a tab kind is listed: - app.js: create at startup and on Reset, icon, visibility defaults, + menu, fixed mode, no ink layer, tool handling, OSC kind + information line (the page's search term), phone remote scroll / tap / page buttons, OSC show / reload / home actions, live setting changes, translation for tapped references. - quick-dock, tool-wheel, presentation-tools: no drawing tools on the tab. - Phone remote: tab label, one finger scrolls the page (taps still tap), hint text; mirror-stream.js reports the page's scroll for the phone's local preview. - Follower screen: the stage publishes a script-free copy of the page's DOM (only while a follower is linked), its scroll and form fields; the follower shows it in sandboxed, double-buffered frames; the relay keeps the page for late joiners. - OSC: /bibleapp/browser, /bibleapp/browser/reload, /bibleapp/browser/home; tab_kind/browser feedback. Admin and phone allow-lists updated. - Settings: Tab Visibility, a Browser Tab section (start page, page size, scripture taps, return to start page), Service Profiles carry its settings, OSC references. - Admin console: show the tab, reload / start page, start page and page size. - BROWSER_TAB.md documents it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,63 @@ async function startLocalHttpServer() {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The Browser tab (browser-tab.js) shows a third-party web page inside the stage window, in an Electron
|
||||
* <webview>. A page from the internet gets none of the app's powers, whatever the renderer asked for:
|
||||
*
|
||||
* - Its preferences are FORCED here, not trusted from the tag: no Node, context isolation, sandbox,
|
||||
* and the one preload that serves the Bible app (browser-guest-preload.cjs). Only the Browser tab's
|
||||
* own session may be used, and only http(s) pages may load.
|
||||
* - Nothing it does can put anything else on the stage: new windows open in the same view, other URL
|
||||
* schemes (mailto:, file:, custom apps) are refused, permission prompts (camera, notifications,
|
||||
* location, fullscreen…) are denied without asking, downloads are cancelled, and pinch-zoom is off
|
||||
* so a stray two-finger touch cannot blow the page up in front of the room.
|
||||
* - It identifies as plain Chrome. Some sites refuse a browser that says "Electron"; this is Chrome's
|
||||
* engine, so the claim is accurate.
|
||||
*/
|
||||
const BROWSER_TAB_PARTITION = 'persist:browser-tab';
|
||||
const isWebUrl = (u) => /^https?:\/\//i.test(String(u || ''));
|
||||
function secureBrowserTab(win) {
|
||||
const { session } = require('electron');
|
||||
const ses = session.fromPartition(BROWSER_TAB_PARTITION);
|
||||
// Electron's agent is "... (KHTML, like Gecko) <app name>/<version> Chrome/x Electron/y Safari/z": keep only Chrome's.
|
||||
try { ses.setUserAgent(ses.getUserAgent().replace(/(\(KHTML, like Gecko\))\s+.*?\s+(Chrome\/)/, '$1 $2').replace(/\s+Electron\/\S+/i, '')); } catch (_) {}
|
||||
ses.setPermissionRequestHandler((_wc, _permission, callback) => callback(false));
|
||||
ses.setPermissionCheckHandler(() => false);
|
||||
ses.on('will-download', (e) => { try { e.preventDefault(); } catch (_) {} });
|
||||
|
||||
win.webContents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
if (params.partition !== BROWSER_TAB_PARTITION || !isWebUrl(params.src)) {
|
||||
console.warn('[BROWSER TAB] refused a webview:', params.partition, params.src);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
delete webPreferences.preloadURL;
|
||||
webPreferences.preload = path.join(__dirname, 'browser-guest-preload.cjs');
|
||||
webPreferences.nodeIntegration = false;
|
||||
webPreferences.nodeIntegrationInSubFrames = false;
|
||||
webPreferences.contextIsolation = true;
|
||||
webPreferences.sandbox = true;
|
||||
webPreferences.webSecurity = true;
|
||||
webPreferences.backgroundThrottling = false; // a page in the background keeps loading, ready when shown
|
||||
});
|
||||
|
||||
win.webContents.on('did-attach-webview', (_event, contents) => {
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (isWebUrl(url)) { try { contents.loadURL(url); } catch (_) {} }
|
||||
return { action: 'deny' };
|
||||
});
|
||||
contents.on('will-navigate', (e, url) => { if (!isWebUrl(url)) e.preventDefault(); });
|
||||
// A page's "Leave site?" guard would otherwise silently cancel Reload and Start page — the very
|
||||
// buttons used to recover the tab in front of the room. They always go through.
|
||||
contents.on('will-prevent-unload', (e) => { try { e.preventDefault(); } catch (_) {} });
|
||||
contents.on('will-redirect', (e, url) => { if (!isWebUrl(url)) e.preventDefault(); });
|
||||
const noPinch = () => { try { contents.setVisualZoomLevelLimits(1, 1); } catch (_) {} };
|
||||
noPinch();
|
||||
contents.on('did-finish-load', noPinch);
|
||||
});
|
||||
}
|
||||
|
||||
async function createWindow () {
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
@@ -205,6 +262,8 @@ async function createWindow () {
|
||||
webSecurity: true, // Same-origin policy ON. App content is served over http://localhost,
|
||||
// so file:// access is unnecessary; external APIs used are CORS-enabled.
|
||||
backgroundThrottling: false, // never throttle timers/paint when idle — first tap always registers
|
||||
// The Browser tab's pages (browser-tab.js). Every <webview> is locked down in will-attach-webview below.
|
||||
webviewTag: true,
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
devTools: true // Enable DevTools for debugging
|
||||
}
|
||||
@@ -256,6 +315,8 @@ async function createWindow () {
|
||||
// Build a minimal Help menu with Content Manager and About options
|
||||
try { setupAppMenu(win); } catch (_) {}
|
||||
|
||||
try { secureBrowserTab(win); } catch (e) { console.warn('[BROWSER TAB] setup failed:', e && e.message); }
|
||||
|
||||
// Start the embedded server and load the app via HTTP so relative API routes work
|
||||
await startLocalHttpServer();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user