add hardening, zoom and useragent options

This commit is contained in:
2026-10-17 10:35:14 +02:00
parent ead23646b2
commit 495a0696ba
15 changed files with 724 additions and 70 deletions
+12 -3
View File
@@ -33,8 +33,14 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
3. Insert mode passes keys through to the page.
4. Hint mode swallows every key.
5. Normal mode goes to `handleNormalModeKey`. Bindings are `if (text == "x")` chains, and two-key sequences (`gg`) use `m_pendingKeys`.
- **Never change the page's size from UI chrome.** Every view resize makes the page re-layout, which on YouTube visibly jumps the player.
- `CompletionView` is not in the layout. It floats over the bottom of the view (`placeCompletion`, re-run on the view's `Resize`).
- `StatusBar` and `CommandLine` share one fixed height.
- Open the command line only through `openCommandLine`, which hides the status bar *before* showing the command line. Showing a widget re-runs the layout at once, so the other order shrinks and regrows the page.
- **`setMode` is where entering and leaving a mode takes effect.** It updates the status line and focus. Leaving `Hint` also runs the hint-clear script.
- **Page interaction goes through `page()->runJavaScript`.** Scrolling, for example, is done with JS calls rather than Qt APIs.
- **Page interaction mostly goes through `page()->runJavaScript`**, except for input that should feel native:
- Scrolling (`scrollBy`, used by `j/k/h/l`, `Ctrl-d/u/f/b` and `gg/G`) sends a pixel-delta `QWheelEvent` to `m_view->focusProxy()` at the pointer, which is what a touchpad sends. Chromium then scrolls whatever is under the pointer. `window.scrollBy` only moves the document, so it did nothing on sites that scroll an inner element.
- Hint clicks are real mouse events (see below).
- **Hint mode** (`src/hints.cpp`):
- The state lives in JS (`window.__attHints`), injected into `QWebEngineScript::ApplicationWorld`. That world shares the DOM with the page but keeps its JS globals isolated from the page's scripts.
- C++ sends one call per key (`keyScript` / `backspaceScript` / `clearScript`). JS returns a `{state: ...}` map, and `handleHintResult` acts on it. That map is the protocol, and it is documented in `hints.h`.
@@ -51,10 +57,13 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
- **History** (`History`, `src/history.cpp`) is SQLite through QtSql, run synchronously on the GUI thread with WAL. `visits` is an append-only log; `urls` (one row per URL, upserted) is what completion searches. Visits are recorded on `loadFinished(true)`, not `urlChanged`. URLs are stored as `toDisplayString()` with the fragment removed, so a `titleChanged` after an in-page jump still updates the right row.
- **`:open` completion:** `BrowserWindow::updateCompletion` runs on the command line's `textEdited` and on entering command mode. It parses `:open`/`:o` and shows `History::search` results in `CompletionView`, which never takes focus. `CommandLine::event` intercepts Tab/Backtab, because `QWidget::event` would otherwise turn them into focus changes. Picking an entry uses `setCommandText`, which doesn't emit `textEdited`, so it doesn't re-filter.
- **Greasemonkey** (`Greasemonkey`, `src/greasemonkey.cpp`, part of the `Session`) inserts each `greasemonkey/*.js` into `profile->scripts()` as a `QWebEngineScript` named `att-greasemonkey:<file>`, in `MainWorld`.
- URL matching is left to QtWebEngine, which parses the `==UserScript==` block during `setSourceCode` and checks `@include/@exclude/@match` in Chromium. So the block must stay in the source, and `setInjectionPoint` must come *after* `setSourceCode`.
- att's own `parseMetadata` is for run-at, noframes, `@require` (cached by SHA-1 of the URL) and `GM_info`.
- URL matching is left to QtWebEngine, which parses the `==UserScript==` block during `setSourceCode` and checks `@include/@exclude/@match` in Chromium. So the block must stay in the source. `setSourceCode` also overwrites the script's name (with `@name`) and injection point, so `setName` and `setInjectionPoint` must come *after* it. `install()` finds the old scripts by the `att-greasemonkey:` name prefix to remove them; a lost prefix makes scripts run twice after a reload.
- att's own `parseMetadata` is for run-at, noframes, `@require` and `GM_info`. A `@require` is cached by the SHA-1 of its URL. It must be `https`, and an optional `#sha256=` pin is checked against the cached file on every install (`requireProblem`).
- Scripts are wrapped in an IIFE (`kWrapperHead`/`kWrapperTail`) that defines the GM API as locals.
- At `document-start`, `document.documentElement` is still null, which the GM helpers must tolerate.
- **Hardening** (`src/hardening.cpp`): `applyToProfile` sets the restrictive `QWebEngineSettings` and the third-party cookie filter. The filter runs on the network thread and must stay a pure function. `main.cpp` applies it right after creating the profile, and warns in the first window if `sandboxDisabledReason` finds the sandbox off. `StatusBar::setUrl` takes a `QUrl` so it can mark plain-HTTP pages (`insecure` property).
- **User agent** (`src/useragent.cpp`): `useragent::apply` sets the profile's UA string *and* its `QWebEngineClientHints`, so the two don't contradict each other. Chromium sends the low-entropy hints (brands, platform, mobile) even with `setAllClientHintsEnabled(false)`, so every preset sets a platform; the brand ("Google Chrome", "Microsoft Edge") goes in through `setFullVersionList`. Chrome-based presets use the engine's own major version. `:useragent` completion reuses `CompletionView` with name / string columns.
- **Settings** live as constants in `src/config.h` (`config::kZoomDefault`, `config::kZoomLevels`, `kBlockThirdPartyCookies`, `kPdfViewer`, `kUserAgent`), named after qutebrowser's settings, until there's a config file. Zoom is per window: `setZoom` / `zoomStep` in `BrowserWindow`, applied before the first load. QtWebEngine keeps a view's zoom across navigations, so it doesn't need re-applying.
- The UI stylesheet is the `kStyleSheet` string in `src/main.cpp`. Widgets are styled by `objectName` and by dynamic properties: `StatusBar` toggles `error` and re-polishes the label to pick up the change.
**Windows, not tabs.** att deliberately has no tab bar, because the user's window manager does the tabbing. Map qutebrowser's tab features onto top-level windows: `d` closes the window, `O` / `:open -w` (or `-t`) opens a new one. Each `BrowserWindow` is one page, with `WA_DeleteOnClose`. All windows share a `Session` (profile, `AdBlocker`, `History`) that `main()` owns. After `app.exec()`, `main()` deletes any window still alive before the history and profile go away. `:quit` closes all windows; `:close` closes one.