add hardening, zoom and useragent options
This commit is contained in:
@@ -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.
|
3. Insert mode passes keys through to the page.
|
||||||
4. Hint mode swallows every key.
|
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`.
|
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.
|
- **`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`):
|
- **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.
|
- 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`.
|
- 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.
|
- **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.
|
- **`: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`.
|
- **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`.
|
- 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` (cached by SHA-1 of the URL) and `GM_info`.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
**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.
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ qt_add_executable(att
|
|||||||
src/commandline.h
|
src/commandline.h
|
||||||
src/completionview.cpp
|
src/completionview.cpp
|
||||||
src/completionview.h
|
src/completionview.h
|
||||||
|
src/config.h
|
||||||
src/greasemonkey.cpp
|
src/greasemonkey.cpp
|
||||||
src/greasemonkey.h
|
src/greasemonkey.h
|
||||||
|
src/hardening.cpp
|
||||||
|
src/hardening.h
|
||||||
src/hints.cpp
|
src/hints.cpp
|
||||||
src/hints.h
|
src/hints.h
|
||||||
src/history.cpp
|
src/history.cpp
|
||||||
@@ -46,6 +49,8 @@ qt_add_executable(att
|
|||||||
src/statusbar.h
|
src/statusbar.h
|
||||||
src/urlutil.cpp
|
src/urlutil.cpp
|
||||||
src/urlutil.h
|
src/urlutil.h
|
||||||
|
src/useragent.cpp
|
||||||
|
src/useragent.h
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(att PRIVATE src adblock)
|
target_include_directories(att PRIVATE src adblock)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ is hidden while a page is fullscreen.
|
|||||||
| `H` / `L` | back / forward in history |
|
| `H` / `L` | back / forward in history |
|
||||||
| `r` / `R` | reload / reload bypassing cache |
|
| `r` / `R` | reload / reload bypassing cache |
|
||||||
| `y` | yank the current URL to the clipboard |
|
| `y` | yank the current URL to the clipboard |
|
||||||
| `+` `-` `=` | zoom in / out / reset |
|
| `+` `-` `=` | zoom in / out a level, back to default |
|
||||||
|
|
||||||
The command line keeps a history (`Up`/`Down`) and aborts on `Escape`, `Ctrl-c`,
|
The command line keeps a history (`Up`/`Down`) and aborts on `Escape`, `Ctrl-c`,
|
||||||
or if you delete the leading `:`.
|
or if you delete the leading `:`.
|
||||||
@@ -77,7 +77,8 @@ or if you delete the leading `:`.
|
|||||||
While typing `:open` (or `:o`), a completion list of previously visited pages
|
While typing `:open` (or `:o`), a completion list of previously visited pages
|
||||||
appears above it, most recent first. Every word you type must appear in the URL
|
appears above it, most recent first. Every word you type must appear in the URL
|
||||||
or the title. `Tab` / `Shift-Tab` cycle through the list and put the selected URL
|
or the title. `Tab` / `Shift-Tab` cycle through the list and put the selected URL
|
||||||
on the command line; `Enter` opens it.
|
on the command line; `Enter` opens it. `:useragent` completes preset names the
|
||||||
|
same way.
|
||||||
|
|
||||||
## Hint mode
|
## Hint mode
|
||||||
|
|
||||||
@@ -134,17 +135,82 @@ the page. Like qutebrowser, att:
|
|||||||
`GM_setClipboard`, `GM_log`, `GM_notification` and the promise-based `GM.*`
|
`GM_setClipboard`, `GM_log`, `GM_notification` and the promise-based `GM.*`
|
||||||
versions.
|
versions.
|
||||||
|
|
||||||
|
`@require` must be `https://`; see *Security* for pinning with `#sha256=`.
|
||||||
|
|
||||||
Limits: stored values live in the site's `localStorage`, so they are per site
|
Limits: stored values live in the site's `localStorage`, so they are per site
|
||||||
rather than shared across sites. `GM_xmlhttpRequest` is a plain XHR, so it can't
|
rather than shared across sites. `GM_xmlhttpRequest` is a plain XHR, so it can't
|
||||||
make cross-origin requests the server doesn't allow (CORS). `@resource` isn't
|
make cross-origin requests the server doesn't allow (CORS). `@resource` isn't
|
||||||
supported. `GM_openInTab` does nothing until new-window requests are handled.
|
supported. `GM_openInTab` does nothing until new-window requests are handled.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
On top of Chromium's own protections, att:
|
||||||
|
|
||||||
|
- blocks third-party cookies, which stops most cross-site tracking. A few embedded
|
||||||
|
logins (e.g. some comment widgets) may stop working.
|
||||||
|
- doesn't let local HTML files (`file://`) read other local files. QtWebEngine
|
||||||
|
allows that by default, so opening a malicious downloaded page could leak e.g.
|
||||||
|
`~/.ssh`.
|
||||||
|
- keeps WebRTC from revealing your LAN address, and doesn't let scripts open
|
||||||
|
pop-up windows.
|
||||||
|
- pins the safe defaults for mixed content, clipboard access and plugins.
|
||||||
|
- shows plain `http://` URLs in orange in the status bar.
|
||||||
|
- only accepts Greasemonkey `@require` files over HTTPS. A `@require` URL may pin
|
||||||
|
its content as in Tampermonkey (`…#sha256=<hex or base64>` or `…#sha256-<base64>`),
|
||||||
|
and the script is skipped if the file doesn't match.
|
||||||
|
- warns at startup if Chromium's sandbox has been disabled
|
||||||
|
(`QTWEBENGINE_DISABLE_SANDBOX`, `--no-sandbox`).
|
||||||
|
|
||||||
|
The engine's security fixes come from QtWebEngine, so keep it current with
|
||||||
|
`nix flake update`. `:version` shows the Chromium version and the security patch
|
||||||
|
level it carries.
|
||||||
|
|
||||||
|
## User agent
|
||||||
|
|
||||||
|
`:useragent <preset>` (alias `:ua`) changes the browser identity that every
|
||||||
|
window sends; with no name it shows the current one. Requests use the new one at
|
||||||
|
once, and `navigator.userAgent` from each page's next load. The client hints
|
||||||
|
(`Sec-CH-UA*`, `navigator.userAgentData`) are set to match the preset's brand
|
||||||
|
and platform.
|
||||||
|
|
||||||
|
| Preset | Claims to be |
|
||||||
|
| ----------------- | ------------------------------------------------- |
|
||||||
|
| `chrome-linux` | Chrome on Linux (the default) |
|
||||||
|
| `chrome-windows` | Chrome on Windows |
|
||||||
|
| `chrome-mac` | Chrome on macOS |
|
||||||
|
| `chrome-android` | Chrome on Android (sites may serve mobile pages) |
|
||||||
|
| `edge-windows` | Edge on Windows |
|
||||||
|
| `firefox-linux`, `firefox-windows`, `firefox-mac` | Firefox |
|
||||||
|
| `safari-mac`, `safari-iphone` | Safari |
|
||||||
|
| `qtwebengine` | QtWebEngine's own string, which names it |
|
||||||
|
|
||||||
|
Chrome and Edge claim the engine's real Chromium major version (as qutebrowser
|
||||||
|
does), so sites don't use features the engine lacks. The Firefox and Safari
|
||||||
|
versions are pinned in `src/useragent.cpp`. Those presets only change what att
|
||||||
|
says, not how it behaves: Chromium's low-entropy client hints still name
|
||||||
|
Chromium, and feature detection still sees Chromium.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
att has no config file yet. The settings live in `src/config.h`, named after
|
||||||
|
their qutebrowser equivalents; change them there and rebuild:
|
||||||
|
|
||||||
|
| Setting | Default | Meaning |
|
||||||
|
| -------------- | ------------------------------- | ---------------------------------------- |
|
||||||
|
| `zoom.default` | 125% | zoom of new windows; `=` returns to it |
|
||||||
|
| `zoom.levels` | 25%, 33%, 50%, … 125%, 150%, … 500% | the steps `+` / `-` move through |
|
||||||
|
| `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` |
|
||||||
|
| `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer |
|
||||||
|
| `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att starts with |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
`:open [-w] <url|query>` (alias `:o`; `-w`, or qutebrowser's `-t`, opens a new
|
`:open [-w] <url|query>` (alias `:o`; `-w`, or qutebrowser's `-t`, opens a new
|
||||||
window), `:close` (alias `:tab-close`, closes this window), `:quit` (alias `:q`,
|
window), `:close` (alias `:tab-close`, closes this window), `:quit` (alias `:q`,
|
||||||
closes all windows), `:reload`, `:back`, `:forward`, `:stop`, `:history-clear`,
|
closes all windows), `:reload`, `:back`, `:forward`, `:stop`, `:history-clear`,
|
||||||
`:greasemonkey-reload`, `:adblock-update`, `:adblock-toggle`.
|
`:greasemonkey-reload`, `:adblock-update`, `:adblock-toggle`, `:zoom [level]`
|
||||||
|
(`150` or `150%`; no level means the default), `:zoom-in`, `:zoom-out`,
|
||||||
|
`:useragent [preset]` (alias `:ua`), `:version`.
|
||||||
|
|
||||||
`:open` accepts a full URL, a bare host (`example.com` → `https://example.com`),
|
`:open` accepts a full URL, a bare host (`example.com` → `https://example.com`),
|
||||||
an existing local path, or free text, which goes to DuckDuckGo.
|
an existing local path, or free text, which goes to DuckDuckGo.
|
||||||
@@ -159,13 +225,15 @@ an existing local path, or free text, which goes to DuckDuckGo.
|
|||||||
| `src/browserwindow` | the window, mode state machine, key dispatch, commands |
|
| `src/browserwindow` | the window, mode state machine, key dispatch, commands |
|
||||||
| `src/commandline` | the `:` prompt |
|
| `src/commandline` | the `:` prompt |
|
||||||
| `src/greasemonkey` | userscript loading and the GM_* API wrapper |
|
| `src/greasemonkey` | userscript loading and the GM_* API wrapper |
|
||||||
|
| `src/hardening` | profile security settings, sandbox check |
|
||||||
| `src/history` | SQLite browsing history |
|
| `src/history` | SQLite browsing history |
|
||||||
| `src/completionview`| the `:open` completion list |
|
| `src/completionview`| the `:open` / `:useragent` completion list |
|
||||||
| `src/statusbar` | mode / URL / progress line |
|
| `src/statusbar` | mode / URL / progress line |
|
||||||
| `src/adblock` | request interceptor around the adblock engine |
|
| `src/adblock` | request interceptor around the adblock engine |
|
||||||
| `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion |
|
| `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion |
|
||||||
| `src/hints` | the injected hint-mode script |
|
| `src/hints` | the injected hint-mode script |
|
||||||
| `src/urlutil` | "what the user typed" → `QUrl` |
|
| `src/urlutil` | "what the user typed" → `QUrl` |
|
||||||
|
| `src/useragent` | user-agent presets and matching client hints |
|
||||||
|
|
||||||
Key events are intercepted with an **application-wide event filter**
|
Key events are intercepted with an **application-wide event filter**
|
||||||
(`BrowserWindow::eventFilter`) rather than `QWidget::keyPressEvent`, because
|
(`BrowserWindow::eventFilter`) rather than `QWidget::keyPressEvent`, because
|
||||||
|
|||||||
+184
-30
@@ -2,33 +2,40 @@
|
|||||||
|
|
||||||
#include "adblock.h"
|
#include "adblock.h"
|
||||||
#include "commandline.h"
|
#include "commandline.h"
|
||||||
|
#include "config.h"
|
||||||
#include "completionview.h"
|
#include "completionview.h"
|
||||||
#include "greasemonkey.h"
|
#include "greasemonkey.h"
|
||||||
#include "hints.h"
|
#include "hints.h"
|
||||||
#include "history.h"
|
#include "history.h"
|
||||||
#include "statusbar.h"
|
#include "statusbar.h"
|
||||||
#include "urlutil.h"
|
#include "urlutil.h"
|
||||||
|
#include "useragent.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
|
#include <QCursor>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QWebEngineFullScreenRequest>
|
#include <QWebEngineFullScreenRequest>
|
||||||
|
#include <QtWebEngineCore/qtwebenginecoreglobal.h>
|
||||||
#include <QWebEnginePage>
|
#include <QWebEnginePage>
|
||||||
#include <QWebEngineProfile>
|
#include <QWebEngineProfile>
|
||||||
#include <QWebEngineScript>
|
#include <QWebEngineScript>
|
||||||
#include <QWebEngineSettings>
|
#include <QWebEngineSettings>
|
||||||
#include <QWebEngineView>
|
#include <QWebEngineView>
|
||||||
|
#include <QWheelEvent>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr int kScrollStep = 60;
|
constexpr int kScrollStep = 60;
|
||||||
constexpr double kZoomStep = 0.1;
|
|
||||||
constexpr int kCompletionLimit = 100;
|
constexpr int kCompletionLimit = 100;
|
||||||
|
// What QtWebEngine accepts.
|
||||||
|
constexpr double kZoomMin = 0.25;
|
||||||
|
constexpr double kZoomMax = 5.0;
|
||||||
|
|
||||||
// ":open [-w|-t] <query>" or ":o ..."; captures the prefix, flag included,
|
// ":open [-w|-t] <query>" or ":o ..."; captures the prefix, flag included,
|
||||||
// and the query.
|
// and the query.
|
||||||
@@ -38,6 +45,13 @@ const QRegularExpression &openCommand()
|
|||||||
return re;
|
return re;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ":useragent <preset>" or ":ua ..."; captures the prefix and the name.
|
||||||
|
const QRegularExpression &userAgentCommand()
|
||||||
|
{
|
||||||
|
static const QRegularExpression re(QStringLiteral("^(:(?:useragent|ua) +)(.*)$"));
|
||||||
|
return re;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent)
|
BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent)
|
||||||
@@ -67,6 +81,7 @@ BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidg
|
|||||||
qApp->installEventFilter(this);
|
qApp->installEventFilter(this);
|
||||||
|
|
||||||
setMode(Mode::Normal);
|
setMode(Mode::Normal);
|
||||||
|
setZoom(config::kZoomDefault, false);
|
||||||
m_view->setUrl(startUrl);
|
m_view->setUrl(startUrl);
|
||||||
focusView();
|
focusView();
|
||||||
}
|
}
|
||||||
@@ -81,18 +96,29 @@ void BrowserWindow::setupUi()
|
|||||||
layout->setContentsMargins(0, 0, 0, 0);
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
layout->setSpacing(0);
|
layout->setSpacing(0);
|
||||||
layout->addWidget(m_view, 1);
|
layout->addWidget(m_view, 1);
|
||||||
layout->addWidget(m_completion);
|
|
||||||
layout->addWidget(m_status);
|
layout->addWidget(m_status);
|
||||||
layout->addWidget(m_commandLine);
|
layout->addWidget(m_commandLine);
|
||||||
setCentralWidget(central);
|
setCentralWidget(central);
|
||||||
|
|
||||||
|
// Nothing that comes and goes may change the page's size: every resize
|
||||||
|
// makes the page re-layout, which on YouTube means the player visibly
|
||||||
|
// jumps. So the completion floats over the bottom of the page instead of
|
||||||
|
// taking space in the layout (see placeCompletion), and the status bar and
|
||||||
|
// the command line, which replace each other, get the same fixed height.
|
||||||
|
m_completion->setParent(central);
|
||||||
|
m_status->ensurePolished();
|
||||||
|
m_commandLine->ensurePolished();
|
||||||
|
const int barHeight = qMax(m_status->sizeHint().height(), m_commandLine->sizeHint().height());
|
||||||
|
m_status->setFixedHeight(barHeight);
|
||||||
|
m_commandLine->setFixedHeight(barHeight);
|
||||||
|
|
||||||
m_commandLine->hide();
|
m_commandLine->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::connectView()
|
void BrowserWindow::connectView()
|
||||||
{
|
{
|
||||||
connect(m_view, &QWebEngineView::urlChanged, this, [this](const QUrl &url) {
|
connect(m_view, &QWebEngineView::urlChanged, this, [this](const QUrl &url) {
|
||||||
m_status->setUrl(url.toDisplayString());
|
m_status->setUrl(url);
|
||||||
// The hints belong to the page that is going away.
|
// The hints belong to the page that is going away.
|
||||||
if (m_mode == Mode::Hint)
|
if (m_mode == Mode::Hint)
|
||||||
setMode(Mode::Normal);
|
setMode(Mode::Normal);
|
||||||
@@ -124,6 +150,11 @@ void BrowserWindow::connectView()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::showMessage(const QString &text, bool isError)
|
||||||
|
{
|
||||||
|
m_status->showMessage(text, isError);
|
||||||
|
}
|
||||||
|
|
||||||
void BrowserWindow::setMode(Mode mode)
|
void BrowserWindow::setMode(Mode mode)
|
||||||
{
|
{
|
||||||
if (m_mode == Mode::Hint && mode != Mode::Hint)
|
if (m_mode == Mode::Hint && mode != Mode::Hint)
|
||||||
@@ -181,6 +212,9 @@ void BrowserWindow::updateStatusVisibility()
|
|||||||
|
|
||||||
bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
|
bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
|
||||||
{
|
{
|
||||||
|
if (watched == m_view && event->type() == QEvent::Resize && m_completion->isVisible())
|
||||||
|
placeCompletion();
|
||||||
|
|
||||||
if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast<QWidget *>(watched)))
|
if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast<QWidget *>(watched)))
|
||||||
return QMainWindow::eventFilter(watched, event);
|
return QMainWindow::eventFilter(watched, event);
|
||||||
|
|
||||||
@@ -256,10 +290,9 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
}
|
}
|
||||||
if (sequence == QLatin1String("go") || sequence == QLatin1String("gO")) {
|
if (sequence == QLatin1String("go") || sequence == QLatin1String("gO")) {
|
||||||
// Edit the current URL, to open it here or in a new window.
|
// Edit the current URL, to open it here or in a new window.
|
||||||
m_commandLine->openWith((sequence == QLatin1String("go") ? QStringLiteral(":open ")
|
openCommandLine((sequence == QLatin1String("go") ? QStringLiteral(":open ")
|
||||||
: QStringLiteral(":open -w "))
|
: QStringLiteral(":open -w "))
|
||||||
+ m_view->url().toString());
|
+ m_view->url().toString());
|
||||||
setMode(Mode::Command);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true; // Swallow the unknown sequence rather than leaking it.
|
return true; // Swallow the unknown sequence rather than leaking it.
|
||||||
@@ -271,13 +304,11 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (text == QLatin1String("o")) {
|
if (text == QLatin1String("o")) {
|
||||||
m_commandLine->openWith(QStringLiteral(":open "));
|
openCommandLine(QStringLiteral(":open "));
|
||||||
setMode(Mode::Command);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("O")) {
|
if (text == QLatin1String("O")) {
|
||||||
m_commandLine->openWith(QStringLiteral(":open -w "));
|
openCommandLine(QStringLiteral(":open -w "));
|
||||||
setMode(Mode::Command);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("d")) {
|
if (text == QLatin1String("d")) {
|
||||||
@@ -285,8 +316,7 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String(":")) {
|
if (text == QLatin1String(":")) {
|
||||||
m_commandLine->openWith(QStringLiteral(":"));
|
openCommandLine(QStringLiteral(":"));
|
||||||
setMode(Mode::Command);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("i")) {
|
if (text == QLatin1String("i")) {
|
||||||
@@ -340,15 +370,15 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("+")) {
|
if (text == QLatin1String("+")) {
|
||||||
m_view->setZoomFactor(m_view->zoomFactor() + kZoomStep);
|
zoomStep(1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("-")) {
|
if (text == QLatin1String("-")) {
|
||||||
m_view->setZoomFactor(qMax(0.25, m_view->zoomFactor() - kZoomStep));
|
zoomStep(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("=")) {
|
if (text == QLatin1String("=")) {
|
||||||
m_view->setZoomFactor(1.0);
|
setZoom(config::kZoomDefault);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +497,46 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
m_view->forward();
|
m_view->forward();
|
||||||
} else if (name == QLatin1String("stop")) {
|
} else if (name == QLatin1String("stop")) {
|
||||||
m_view->stop();
|
m_view->stop();
|
||||||
|
} else if (name == QLatin1String("version")) {
|
||||||
|
// The Chromium security patch level is what says how far behind
|
||||||
|
// Chrome's fixes this build is; "nix flake update" moves it forward.
|
||||||
|
m_status->showMessage(QStringLiteral("att %1, Qt %2, Chromium %3, security patches %4")
|
||||||
|
.arg(QCoreApplication::applicationVersion(),
|
||||||
|
QString::fromLatin1(qVersion()),
|
||||||
|
QString::fromLatin1(qWebEngineChromiumVersion()),
|
||||||
|
QString::fromLatin1(qWebEngineChromiumSecurityPatchVersion())));
|
||||||
|
} else if (name == QLatin1String("zoom")) {
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
setZoom(config::kZoomDefault);
|
||||||
|
} else {
|
||||||
|
// "150" or "150%", like qutebrowser.
|
||||||
|
QString level = args;
|
||||||
|
if (level.endsWith(QLatin1Char('%')))
|
||||||
|
level.chop(1);
|
||||||
|
bool ok = false;
|
||||||
|
const double percent = level.toDouble(&ok);
|
||||||
|
if (ok && percent >= kZoomMin * 100 && percent <= kZoomMax * 100)
|
||||||
|
setZoom(percent / 100);
|
||||||
|
else
|
||||||
|
m_status->showMessage(QStringLiteral("Zoom level must be %1% to %2%: %3")
|
||||||
|
.arg(kZoomMin * 100).arg(kZoomMax * 100).arg(args),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
} else if (name == QLatin1String("zoom-in")) {
|
||||||
|
zoomStep(1);
|
||||||
|
} else if (name == QLatin1String("zoom-out")) {
|
||||||
|
zoomStep(-1);
|
||||||
|
} else if (name == QLatin1String("useragent") || name == QLatin1String("ua")) {
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
m_status->showMessage(QStringLiteral("User agent: %1").arg(useragent::current()));
|
||||||
|
} else if (useragent::apply(m_session.profile, args)) {
|
||||||
|
// Requests use it at once; navigator.userAgent changes with the
|
||||||
|
// next load, so say so rather than leave the page inconsistent.
|
||||||
|
m_status->showMessage(
|
||||||
|
QStringLiteral("User agent: %1 (applies from the next page load)").arg(args));
|
||||||
|
} else {
|
||||||
|
m_status->showMessage(QStringLiteral("Unknown user agent: %1").arg(args), true);
|
||||||
|
}
|
||||||
} else if (name == QLatin1String("history-clear")) {
|
} else if (name == QLatin1String("history-clear")) {
|
||||||
if (m_session.history->clear()) {
|
if (m_session.history->clear()) {
|
||||||
// Also forget Chromium's own record, which colours visited links.
|
// Also forget Chromium's own record, which colours visited links.
|
||||||
@@ -488,28 +558,63 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::openCommandLine(const QString &text)
|
||||||
|
{
|
||||||
|
// Hide the status bar before the command line appears: showing a widget
|
||||||
|
// re-runs the layout immediately, and with both bars visible for a moment
|
||||||
|
// the page would shrink and grow back (two resizes) every time.
|
||||||
|
m_status->hide();
|
||||||
|
m_commandLine->openWith(text);
|
||||||
|
setMode(Mode::Command);
|
||||||
|
}
|
||||||
|
|
||||||
void BrowserWindow::updateCompletion()
|
void BrowserWindow::updateCompletion()
|
||||||
{
|
{
|
||||||
const QRegularExpressionMatch match = openCommand().match(m_commandLine->text());
|
const QString text = m_commandLine->text();
|
||||||
if (!match.hasMatch()) {
|
if (const QRegularExpressionMatch match = openCommand().match(text); match.hasMatch()) {
|
||||||
|
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||||
|
m_completion->setEntries(m_session.history->search(words, kCompletionLimit));
|
||||||
|
} else if (const QRegularExpressionMatch match = userAgentCommand().match(text);
|
||||||
|
match.hasMatch()) {
|
||||||
|
// Preset name in the first column, the string it sends in the second.
|
||||||
|
const QString query = match.captured(2).trimmed();
|
||||||
|
QList<History::Entry> entries;
|
||||||
|
for (const useragent::Preset &preset : useragent::presets()) {
|
||||||
|
if (preset.name.contains(query, Qt::CaseInsensitive))
|
||||||
|
entries.append({preset.name, preset.userAgent});
|
||||||
|
}
|
||||||
|
m_completion->setEntries(entries);
|
||||||
|
} else {
|
||||||
m_completion->setEntries({});
|
m_completion->setEntries({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
placeCompletion();
|
||||||
m_completion->setEntries(m_session.history->search(words, kCompletionLimit));
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::placeCompletion()
|
||||||
|
{
|
||||||
|
// Bottom-aligned over the page, just above the command line.
|
||||||
|
const QRect page = m_view->geometry();
|
||||||
|
const int height = qMin(m_completion->height(), page.height());
|
||||||
|
m_completion->setGeometry(page.left(), page.bottom() + 1 - height, page.width(), height);
|
||||||
|
m_completion->raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::cycleCompletion(int step)
|
void BrowserWindow::cycleCompletion(int step)
|
||||||
{
|
{
|
||||||
if (!m_completion->isVisible())
|
if (!m_completion->isVisible())
|
||||||
return;
|
return;
|
||||||
const QString url = m_completion->cycle(step);
|
const QString choice = m_completion->cycle(step);
|
||||||
if (url.isEmpty())
|
if (choice.isEmpty())
|
||||||
return;
|
return;
|
||||||
// Keep whichever of ":o " / ":open " the user typed.
|
// Keep the command as the user typed it (":o " or ":open ", ":ua " or
|
||||||
const QRegularExpressionMatch match = openCommand().match(m_commandLine->text());
|
// ":useragent ").
|
||||||
|
const QString text = m_commandLine->text();
|
||||||
|
QRegularExpressionMatch match = userAgentCommand().match(text);
|
||||||
|
if (!match.hasMatch())
|
||||||
|
match = openCommand().match(text);
|
||||||
const QString prefix = match.hasMatch() ? match.captured(1) : QStringLiteral(":open ");
|
const QString prefix = match.hasMatch() ? match.captured(1) : QStringLiteral(":open ");
|
||||||
m_commandLine->setCommandText(prefix + url);
|
m_commandLine->setCommandText(prefix + choice);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::openUrl(const QString &input, bool newWindow)
|
void BrowserWindow::openUrl(const QString &input, bool newWindow)
|
||||||
@@ -529,21 +634,70 @@ void BrowserWindow::openUrl(const QString &input, bool newWindow)
|
|||||||
|
|
||||||
void BrowserWindow::scrollBy(int dx, int dy)
|
void BrowserWindow::scrollBy(int dx, int dy)
|
||||||
{
|
{
|
||||||
m_view->page()->runJavaScript(
|
// A pixel-precise wheel event, which is what a touchpad sends, rather than
|
||||||
QStringLiteral("window.scrollBy(%1, %2);").arg(dx).arg(dy));
|
// window.scrollBy(): Chromium then scrolls whatever is under the pointer,
|
||||||
|
// the way the touchpad does. window.scrollBy() only ever moves the
|
||||||
|
// document, so on sites that scroll an inner element instead (most web
|
||||||
|
// apps) it did nothing at all.
|
||||||
|
QWidget *target = m_view->focusProxy();
|
||||||
|
if (!target)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QPointF pos = target->mapFromGlobal(QCursor::pos());
|
||||||
|
if (!target->rect().contains(pos.toPoint()))
|
||||||
|
pos = QRectF(target->rect()).center();
|
||||||
|
|
||||||
|
// Qt's wheel deltas point the way the content moves, the opposite of the
|
||||||
|
// scroll direction.
|
||||||
|
const QPoint pixels(-dx, -dy);
|
||||||
|
QWheelEvent event(pos, target->mapToGlobal(pos), pixels, pixels * 2, Qt::NoButton,
|
||||||
|
Qt::NoModifier, Qt::NoScrollPhase, false);
|
||||||
|
QCoreApplication::sendEvent(target, &event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::scrollPages(double factor)
|
void BrowserWindow::scrollPages(double factor)
|
||||||
{
|
{
|
||||||
m_view->page()->runJavaScript(
|
scrollBy(0, qRound(m_view->height() * factor));
|
||||||
QStringLiteral("window.scrollBy(0, window.innerHeight * %1);").arg(factor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::scrollToEnd(bool bottom)
|
void BrowserWindow::scrollToEnd(bool bottom)
|
||||||
{
|
{
|
||||||
m_view->page()->runJavaScript(
|
// Far more than any page is tall; Chromium stops at the end.
|
||||||
bottom ? QStringLiteral("window.scrollTo(0, document.body.scrollHeight);")
|
constexpr int kFar = 10'000'000;
|
||||||
: QStringLiteral("window.scrollTo(0, 0);"));
|
scrollBy(0, bottom ? kFar : -kFar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::setZoom(double factor, bool announce)
|
||||||
|
{
|
||||||
|
const double zoom = qBound(kZoomMin, factor, kZoomMax);
|
||||||
|
m_view->setZoomFactor(zoom);
|
||||||
|
if (announce)
|
||||||
|
m_status->showMessage(QStringLiteral("Zoom level: %1%").arg(qRound(zoom * 100)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BrowserWindow::zoomStep(int direction)
|
||||||
|
{
|
||||||
|
// The next level strictly beyond the current zoom, so a zoom set with
|
||||||
|
// ":zoom 130" still steps to 150 / 125. The epsilon absorbs 0.33 vs 1/3.
|
||||||
|
constexpr double kEpsilon = 0.001;
|
||||||
|
const double current = m_view->zoomFactor();
|
||||||
|
const auto &levels = config::kZoomLevels;
|
||||||
|
if (direction > 0) {
|
||||||
|
for (double level : levels) {
|
||||||
|
if (level > current + kEpsilon) {
|
||||||
|
setZoom(level);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (auto it = levels.rbegin(); it != levels.rend(); ++it) {
|
||||||
|
if (*it < current - kEpsilon) {
|
||||||
|
setZoom(*it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_status->showMessage(QStringLiteral("Zoom level: %1% (limit)").arg(qRound(current * 100)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::focusView()
|
void BrowserWindow::focusView()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public:
|
|||||||
BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent = nullptr);
|
BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setMode(Mode mode);
|
void setMode(Mode mode);
|
||||||
|
void showMessage(const QString &text, bool isError = false);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Key handling lives in an application-wide filter because QtWebEngine
|
// Key handling lives in an application-wide filter because QtWebEngine
|
||||||
@@ -59,13 +60,17 @@ private:
|
|||||||
void handleHintResult(const QVariantMap &result);
|
void handleHintResult(const QVariantMap &result);
|
||||||
void clickAt(const QPointF &pos);
|
void clickAt(const QPointF &pos);
|
||||||
void runCommand(const QString &command);
|
void runCommand(const QString &command);
|
||||||
|
void openCommandLine(const QString &text);
|
||||||
void updateCompletion();
|
void updateCompletion();
|
||||||
void cycleCompletion(int step);
|
void cycleCompletion(int step);
|
||||||
|
void placeCompletion();
|
||||||
void openUrl(const QString &input, bool newWindow);
|
void openUrl(const QString &input, bool newWindow);
|
||||||
void scrollBy(int dx, int dy);
|
void scrollBy(int dx, int dy);
|
||||||
void scrollPages(double factor);
|
void scrollPages(double factor);
|
||||||
void scrollToEnd(bool bottom);
|
void scrollToEnd(bool bottom);
|
||||||
void focusView();
|
void focusView();
|
||||||
|
void setZoom(double factor, bool announce = true);
|
||||||
|
void zoomStep(int direction);
|
||||||
void setPageFullScreen(bool on);
|
void setPageFullScreen(bool on);
|
||||||
void updateStatusVisibility();
|
void updateStatusVisibility();
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
|
|
||||||
// The list above the command line offering history entries for ":open", with
|
// The list above the command line, like qutebrowser's completion widget: history
|
||||||
// URL and title columns, like qutebrowser's completion widget. It never takes
|
// for ":open" (URL and title columns) or presets for ":useragent" (name and
|
||||||
// focus: keys stay with the CommandLine, which drives it via cycle().
|
// string). It never takes focus: keys stay with the CommandLine, which drives
|
||||||
|
// it via cycle().
|
||||||
class CompletionView : public QTreeWidget
|
class CompletionView : public QTreeWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -17,6 +18,7 @@ public:
|
|||||||
// Shows the entries with nothing selected, or hides the view if empty.
|
// Shows the entries with nothing selected, or hides the view if empty.
|
||||||
void setEntries(const QList<History::Entry> &entries);
|
void setEntries(const QList<History::Entry> &entries);
|
||||||
|
|
||||||
// Moves the selection step rows (wrapping) and returns the selected URL.
|
// Moves the selection step rows (wrapping) and returns the first
|
||||||
|
// column of the selected row.
|
||||||
QString cycle(int step);
|
QString cycle(int step);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
// User-facing settings, named after their qutebrowser equivalents. att has no
|
||||||
|
// config file yet; until it does, change them here and rebuild.
|
||||||
|
namespace config {
|
||||||
|
|
||||||
|
// zoom.default: the zoom every new window starts at, and what "=" and a bare
|
||||||
|
// ":zoom" return to. 1.0 is 100%.
|
||||||
|
inline constexpr double kZoomDefault = 1.25;
|
||||||
|
|
||||||
|
// zoom.levels: the steps "+" / "-" (":zoom-in" / ":zoom-out") move through.
|
||||||
|
// qutebrowser's defaults; QtWebEngine accepts 25% to 500%.
|
||||||
|
inline constexpr std::array kZoomLevels = {
|
||||||
|
0.25, 0.33, 0.50, 0.67, 0.75, 0.90, 1.00, 1.10,
|
||||||
|
1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00,
|
||||||
|
};
|
||||||
|
|
||||||
|
// content.cookies.accept = no-3rdparty: refuse cookies a page's third-party
|
||||||
|
// requests try to set or send. Blocks most cross-site tracking; a few embedded
|
||||||
|
// logins (e.g. some comment widgets) stop working.
|
||||||
|
inline constexpr bool kBlockThirdPartyCookies = true;
|
||||||
|
|
||||||
|
// content.pdfjs (roughly): Chromium's built-in PDF viewer. PDFium is a classic
|
||||||
|
// source of exploitable bugs, but att can't download files, so without it a PDF
|
||||||
|
// can't be opened at all.
|
||||||
|
inline constexpr bool kPdfViewer = true;
|
||||||
|
|
||||||
|
// content.headers.user_agent: the preset (see ":useragent" and useragent.cpp)
|
||||||
|
// att starts with. Chrome on Linux is the most common UA that matches the
|
||||||
|
// engine, so sites treat att as the browser it actually is.
|
||||||
|
inline constexpr auto kUserAgent = "chrome-linux";
|
||||||
|
|
||||||
|
} // namespace config
|
||||||
+88
-14
@@ -253,6 +253,64 @@ QWebEngineScript::InjectionPoint injectionPoint(const QString &runAt)
|
|||||||
return QWebEngineScript::DocumentReady; // Greasemonkey's default.
|
return QWebEngineScript::DocumentReady; // Greasemonkey's default.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @require'd code runs with the script's full power on every page it matches,
|
||||||
|
// so a tampered download is as bad as a malicious script. Only HTTPS is
|
||||||
|
// allowed (downloads also refuse redirects away from HTTPS), and a URL may pin
|
||||||
|
// its content in Tampermonkey's format: "#sha256=<hex or base64>", or the SRI
|
||||||
|
// form "#sha256-<base64>", optionally among other comma/semicolon-separated
|
||||||
|
// hashes, which are ignored.
|
||||||
|
struct RequirePin {
|
||||||
|
QByteArray sha256; // empty: not pinned
|
||||||
|
QString error; // the pin is malformed
|
||||||
|
};
|
||||||
|
|
||||||
|
RequirePin requirePin(const QUrl &url)
|
||||||
|
{
|
||||||
|
static const QRegularExpression part(QStringLiteral("^sha256[=-](.+)$"),
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
RequirePin pin;
|
||||||
|
const QStringList parts = url.fragment(QUrl::FullyDecoded)
|
||||||
|
.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts);
|
||||||
|
for (const QString &p : parts) {
|
||||||
|
const QRegularExpressionMatch m = part.match(p.trimmed());
|
||||||
|
if (!m.hasMatch())
|
||||||
|
continue;
|
||||||
|
const QByteArray value = m.captured(1).toLatin1();
|
||||||
|
static const QRegularExpression hex(QStringLiteral("^[0-9a-fA-F]{64}$"));
|
||||||
|
QByteArray digest;
|
||||||
|
if (hex.match(QString::fromLatin1(value)).hasMatch()) {
|
||||||
|
digest = QByteArray::fromHex(value);
|
||||||
|
} else {
|
||||||
|
const auto decoded = QByteArray::fromBase64Encoding(
|
||||||
|
value, QByteArray::Base64Encoding | QByteArray::AbortOnBase64DecodingErrors);
|
||||||
|
const auto decodedUrl = QByteArray::fromBase64Encoding(
|
||||||
|
value, QByteArray::Base64UrlEncoding | QByteArray::AbortOnBase64DecodingErrors);
|
||||||
|
digest = decoded ? *decoded : decodedUrl ? *decodedUrl : QByteArray();
|
||||||
|
}
|
||||||
|
if (digest.size() != 32) {
|
||||||
|
pin.error = QStringLiteral("malformed sha256 pin");
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
pin.sha256 = digest;
|
||||||
|
}
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why the @require at url can't be used, or an empty string if it can.
|
||||||
|
QString requireProblem(const QUrl &url, const QByteArray &content)
|
||||||
|
{
|
||||||
|
if (url.scheme() != QLatin1String("https"))
|
||||||
|
return QStringLiteral("@require is not https: %1").arg(url.toDisplayString());
|
||||||
|
const RequirePin pin = requirePin(url);
|
||||||
|
if (!pin.error.isEmpty())
|
||||||
|
return QStringLiteral("%1 in %2").arg(pin.error, url.toDisplayString());
|
||||||
|
if (!pin.sha256.isEmpty()
|
||||||
|
&& QCryptographicHash::hash(content, QCryptographicHash::Sha256) != pin.sha256)
|
||||||
|
return QStringLiteral("@require does not match its sha256 pin: %1")
|
||||||
|
.arg(url.toDisplayString(QUrl::RemoveFragment));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Greasemonkey::Metadata Greasemonkey::parseMetadata(const QString &source)
|
Greasemonkey::Metadata Greasemonkey::parseMetadata(const QString &source)
|
||||||
@@ -340,7 +398,9 @@ void Greasemonkey::reload(bool announce)
|
|||||||
if (script.meta.name.isEmpty())
|
if (script.meta.name.isEmpty())
|
||||||
script.meta.name = fileName;
|
script.meta.name = fileName;
|
||||||
for (const QString &url : std::as_const(script.meta.requireUrls)) {
|
for (const QString &url : std::as_const(script.meta.requireUrls)) {
|
||||||
if (!QFile::exists(requirePath(url)) && !missing.contains(url))
|
// install() reports the non-https ones; they are never fetched.
|
||||||
|
if (QUrl(url).scheme() == QLatin1String("https") && !QFile::exists(requirePath(url))
|
||||||
|
&& !missing.contains(url))
|
||||||
missing << url;
|
missing << url;
|
||||||
}
|
}
|
||||||
scripts << script;
|
scripts << script;
|
||||||
@@ -356,7 +416,7 @@ void Greasemonkey::reload(bool announce)
|
|||||||
auto remaining = std::make_shared<int>(int(missing.size()));
|
auto remaining = std::make_shared<int>(int(missing.size()));
|
||||||
auto failed = std::make_shared<QStringList>();
|
auto failed = std::make_shared<QStringList>();
|
||||||
for (const QString &url : std::as_const(missing)) {
|
for (const QString &url : std::as_const(missing)) {
|
||||||
QNetworkRequest request{QUrl(url)};
|
QNetworkRequest request{QUrl(url).adjusted(QUrl::RemoveFragment)};
|
||||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
QNetworkReply *reply = m_network->get(request);
|
QNetworkReply *reply = m_network->get(request);
|
||||||
@@ -387,28 +447,43 @@ void Greasemonkey::install(const QList<Script> &scripts, const QStringList &fail
|
|||||||
QStringList skipped;
|
QStringList skipped;
|
||||||
for (const Script &script : scripts) {
|
for (const Script &script : scripts) {
|
||||||
QString requiredCode;
|
QString requiredCode;
|
||||||
bool complete = true;
|
QString problem;
|
||||||
for (const QString &url : script.meta.requireUrls) {
|
for (const QString &url : script.meta.requireUrls) {
|
||||||
QFile file(requirePath(url));
|
if (failedRequires.contains(url)) {
|
||||||
if (failedRequires.contains(url) || !file.open(QIODevice::ReadOnly)) {
|
problem = QStringLiteral("@require failed to download: %1").arg(url);
|
||||||
complete = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
requiredCode += QString::fromUtf8(file.readAll()) + QLatin1Char('\n');
|
QFile file(requirePath(url));
|
||||||
|
const QByteArray content = file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
||||||
|
problem = requireProblem(QUrl(url), content);
|
||||||
|
if (problem.isEmpty() && !file.isOpen())
|
||||||
|
problem = QStringLiteral("@require missing: %1").arg(url);
|
||||||
|
if (!problem.isEmpty()) {
|
||||||
|
// Checked on every install, so a cache file changed on disk is
|
||||||
|
// caught too. Drop a mismatching one so a fixed upstream file
|
||||||
|
// is fetched again on the next reload.
|
||||||
|
if (file.isOpen())
|
||||||
|
file.remove();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Running a script without the library it needs would just throw.
|
requiredCode += QString::fromUtf8(content) + QLatin1Char('\n');
|
||||||
if (!complete) {
|
}
|
||||||
skipped << script.meta.name;
|
// Running a script without the code it requires would just throw, or
|
||||||
|
// worse, run with code that isn't what its author pinned.
|
||||||
|
if (!problem.isEmpty()) {
|
||||||
|
skipped << QStringLiteral("%1 (%2)").arg(script.meta.name, problem);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QWebEngineScript web;
|
QWebEngineScript web;
|
||||||
web.setName(QString::fromLatin1(kScriptNamePrefix) + script.fileName);
|
|
||||||
web.setWorldId(QWebEngineScript::MainWorld);
|
web.setWorldId(QWebEngineScript::MainWorld);
|
||||||
// setSourceCode first: QtWebEngine parses the ==UserScript== block here,
|
// setSourceCode first: QtWebEngine parses the ==UserScript== block here,
|
||||||
// keeping @include/@exclude/@match for Chromium to check per page, and
|
// keeping @include/@exclude/@match for Chromium to check per page, and
|
||||||
// would overwrite an injection point set earlier.
|
// overwrites the name (with @name) and injection point set earlier. The
|
||||||
|
// name must keep our prefix, or the next reload can't find and remove
|
||||||
|
// this script and it would run twice.
|
||||||
web.setSourceCode(wrap(script.meta, requiredCode, script.source));
|
web.setSourceCode(wrap(script.meta, requiredCode, script.source));
|
||||||
|
web.setName(QString::fromLatin1(kScriptNamePrefix) + script.fileName);
|
||||||
web.setInjectionPoint(injectionPoint(script.meta.runAt));
|
web.setInjectionPoint(injectionPoint(script.meta.runAt));
|
||||||
web.setRunsOnSubFrames(!script.meta.noFrames);
|
web.setRunsOnSubFrames(!script.meta.noFrames);
|
||||||
collection->insert(web);
|
collection->insert(web);
|
||||||
@@ -416,8 +491,7 @@ void Greasemonkey::install(const QList<Script> &scripts, const QStringList &fail
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!skipped.isEmpty())
|
if (!skipped.isEmpty())
|
||||||
emit message(QStringLiteral("Greasemonkey: skipped %1 (an @require failed to download)")
|
emit message(QStringLiteral("Greasemonkey: skipped %1").arg(skipped.join(QLatin1String("; "))),
|
||||||
.arg(skipped.join(QLatin1String(", "))),
|
|
||||||
true);
|
true);
|
||||||
else if (announce)
|
else if (announce)
|
||||||
emit message(QStringLiteral("Greasemonkey: loaded %1 script(s); reload pages to apply").arg(installed),
|
emit message(QStringLiteral("Greasemonkey: loaded %1 script(s); reload pages to apply").arg(installed),
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "hardening.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QLatin1String>
|
||||||
|
#include <QWebEngineCookieStore>
|
||||||
|
#include <QWebEngineProfile>
|
||||||
|
#include <QWebEngineSettings>
|
||||||
|
|
||||||
|
namespace hardening {
|
||||||
|
|
||||||
|
void applyToProfile(QWebEngineProfile *profile)
|
||||||
|
{
|
||||||
|
QWebEngineSettings *s = profile->settings();
|
||||||
|
|
||||||
|
// On by default: a local HTML file (say, a download opened with
|
||||||
|
// ":open ~/Downloads/x.html") could read any other local file, ~/.ssh
|
||||||
|
// included, and send it out.
|
||||||
|
s->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
|
||||||
|
// Keep WebRTC from revealing LAN addresses to sites.
|
||||||
|
s->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);
|
||||||
|
// No pop-ups from script. (Links with target=_blank are a separate path.)
|
||||||
|
s->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
|
||||||
|
s->setAttribute(QWebEngineSettings::PdfViewerEnabled, config::kPdfViewer);
|
||||||
|
|
||||||
|
// Already the defaults; pinned here so a Qt default change can't relax them.
|
||||||
|
s->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, false);
|
||||||
|
s->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false);
|
||||||
|
s->setAttribute(QWebEngineSettings::PluginsEnabled, false);
|
||||||
|
|
||||||
|
if (config::kBlockThirdPartyCookies) {
|
||||||
|
// Runs on the network thread; must stay a pure function of its argument.
|
||||||
|
profile->cookieStore()->setCookieFilter(
|
||||||
|
[](const QWebEngineCookieStore::FilterRequest &request) { return !request.thirdParty; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString sandboxDisabledReason(int argc, char **argv)
|
||||||
|
{
|
||||||
|
if (!qEnvironmentVariableIsEmpty("QTWEBENGINE_DISABLE_SANDBOX"))
|
||||||
|
return QStringLiteral("QTWEBENGINE_DISABLE_SANDBOX is set");
|
||||||
|
if (qgetenv("QTWEBENGINE_CHROMIUM_FLAGS").contains("--no-sandbox"))
|
||||||
|
return QStringLiteral("QTWEBENGINE_CHROMIUM_FLAGS contains --no-sandbox");
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (QLatin1String(argv[i]) == QLatin1String("--no-sandbox"))
|
||||||
|
return QStringLiteral("att was started with --no-sandbox");
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace hardening
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QWebEngineProfile;
|
||||||
|
|
||||||
|
// Security settings QtWebEngine leaves permissive by default. See the README's
|
||||||
|
// "Security" section for what each one guards against.
|
||||||
|
namespace hardening {
|
||||||
|
|
||||||
|
// Per-profile settings and the third-party cookie filter.
|
||||||
|
void applyToProfile(QWebEngineProfile *profile);
|
||||||
|
|
||||||
|
// Why Chromium's sandbox is off, or an empty string if nothing disables it.
|
||||||
|
// The sandbox is what keeps a compromised renderer from reaching your files.
|
||||||
|
QString sandboxDisabledReason(int argc, char **argv);
|
||||||
|
|
||||||
|
} // namespace hardening
|
||||||
+15
-1
@@ -1,8 +1,11 @@
|
|||||||
#include "adblock.h"
|
#include "adblock.h"
|
||||||
#include "browserwindow.h"
|
#include "browserwindow.h"
|
||||||
|
#include "config.h"
|
||||||
#include "greasemonkey.h"
|
#include "greasemonkey.h"
|
||||||
|
#include "hardening.h"
|
||||||
#include "history.h"
|
#include "history.h"
|
||||||
#include "urlutil.h"
|
#include "urlutil.h"
|
||||||
|
#include "useragent.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
@@ -25,6 +28,9 @@ constexpr auto kStyleSheet = R"(
|
|||||||
QLabel#statusUrl[error="true"] {
|
QLabel#statusUrl[error="true"] {
|
||||||
color: #ff6b6b;
|
color: #ff6b6b;
|
||||||
}
|
}
|
||||||
|
QLabel#statusUrl[insecure="true"] {
|
||||||
|
color: #e5a050;
|
||||||
|
}
|
||||||
QLabel#statusProgress {
|
QLabel#statusProgress {
|
||||||
color: #8a8a8a;
|
color: #8a8a8a;
|
||||||
}
|
}
|
||||||
@@ -73,6 +79,8 @@ int main(int argc, char *argv[])
|
|||||||
// Like qutebrowser's content.cookies.store: keep session cookies too, so
|
// Like qutebrowser's content.cookies.store: keep session cookies too, so
|
||||||
// sites that log in with one don't log you out on every restart.
|
// sites that log in with one don't log you out on every restart.
|
||||||
profile->setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies);
|
profile->setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies);
|
||||||
|
hardening::applyToProfile(profile);
|
||||||
|
useragent::apply(profile, QString::fromLatin1(config::kUserAgent));
|
||||||
|
|
||||||
// Created after the profile so it is also destroyed after it: the profile
|
// Created after the profile so it is also destroyed after it: the profile
|
||||||
// does not own its interceptor and must not outlive it.
|
// does not own its interceptor and must not outlive it.
|
||||||
@@ -85,7 +93,13 @@ int main(int argc, char *argv[])
|
|||||||
const Session session{profile, adBlocker, greasemonkey, &history};
|
const Session session{profile, adBlocker, greasemonkey, &history};
|
||||||
|
|
||||||
// Windows delete themselves on close; the app quits with the last one.
|
// Windows delete themselves on close; the app quits with the last one.
|
||||||
(new BrowserWindow(session, startUrl))->show();
|
auto *firstWindow = new BrowserWindow(session, startUrl);
|
||||||
|
firstWindow->show();
|
||||||
|
const QString sandboxOff = hardening::sandboxDisabledReason(argc, argv);
|
||||||
|
if (!sandboxOff.isEmpty()) {
|
||||||
|
qWarning("att: Chromium's sandbox is disabled: %s", qPrintable(sandboxOff));
|
||||||
|
firstWindow->showMessage(QStringLiteral("Sandbox disabled: %1").arg(sandboxOff), true);
|
||||||
|
}
|
||||||
// After the window exists, so its status bar shows their messages. The
|
// After the window exists, so its status bar shows their messages. The
|
||||||
// scripts are in place before the event loop starts the first page load.
|
// scripts are in place before the event loop starts the first page load.
|
||||||
adBlocker->start();
|
adBlocker->start();
|
||||||
|
|||||||
+16
-11
@@ -34,10 +34,12 @@ void StatusBar::setMode(const QString &mode)
|
|||||||
m_mode->setVisible(!mode.isEmpty());
|
m_mode->setVisible(!mode.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusBar::setUrl(const QString &url)
|
void StatusBar::setUrl(const QUrl &url)
|
||||||
{
|
{
|
||||||
m_urlText = url;
|
m_urlText = url.toDisplayString();
|
||||||
m_url->setText(url);
|
m_insecure = url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("ws");
|
||||||
|
setUrlLabelState(false, m_insecure);
|
||||||
|
m_url->setText(m_urlText);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusBar::setProgress(int percent)
|
void StatusBar::setProgress(int percent)
|
||||||
@@ -49,20 +51,23 @@ void StatusBar::setProgress(int percent)
|
|||||||
|
|
||||||
void StatusBar::showMessage(const QString &message, bool isError)
|
void StatusBar::showMessage(const QString &message, bool isError)
|
||||||
{
|
{
|
||||||
m_url->setProperty("error", isError);
|
setUrlLabelState(isError, false);
|
||||||
m_url->setText(message);
|
m_url->setText(message);
|
||||||
|
|
||||||
// Re-polish so the stylesheet picks up the changed property.
|
|
||||||
m_url->style()->unpolish(m_url);
|
|
||||||
m_url->style()->polish(m_url);
|
|
||||||
|
|
||||||
QTimer::singleShot(4000, this, &StatusBar::clearMessage);
|
QTimer::singleShot(4000, this, &StatusBar::clearMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusBar::clearMessage()
|
void StatusBar::clearMessage()
|
||||||
{
|
{
|
||||||
m_url->setProperty("error", false);
|
setUrlLabelState(false, m_insecure);
|
||||||
m_url->style()->unpolish(m_url);
|
|
||||||
m_url->style()->polish(m_url);
|
|
||||||
m_url->setText(m_urlText);
|
m_url->setText(m_urlText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StatusBar::setUrlLabelState(bool error, bool insecure)
|
||||||
|
{
|
||||||
|
m_url->setProperty("error", error);
|
||||||
|
m_url->setProperty("insecure", insecure);
|
||||||
|
// Re-polish so the stylesheet picks up the changed properties.
|
||||||
|
m_url->style()->unpolish(m_url);
|
||||||
|
m_url->style()->polish(m_url);
|
||||||
|
}
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QUrl>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@@ -14,15 +15,20 @@ public:
|
|||||||
explicit StatusBar(QWidget *parent = nullptr);
|
explicit StatusBar(QWidget *parent = nullptr);
|
||||||
|
|
||||||
void setMode(const QString &mode);
|
void setMode(const QString &mode);
|
||||||
void setUrl(const QString &url);
|
// Plain-HTTP URLs are shown in a warning colour: anyone on the network
|
||||||
|
// can read and change those pages.
|
||||||
|
void setUrl(const QUrl &url);
|
||||||
void setProgress(int percent);
|
void setProgress(int percent);
|
||||||
// Transient feedback: errors, unknown commands, yanked URLs.
|
// Transient feedback: errors, unknown commands, yanked URLs.
|
||||||
void showMessage(const QString &message, bool isError = false);
|
void showMessage(const QString &message, bool isError = false);
|
||||||
void clearMessage();
|
void clearMessage();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void setUrlLabelState(bool error, bool insecure);
|
||||||
|
|
||||||
QLabel *m_mode;
|
QLabel *m_mode;
|
||||||
QLabel *m_url;
|
QLabel *m_url;
|
||||||
QLabel *m_progress;
|
QLabel *m_progress;
|
||||||
QString m_urlText;
|
QString m_urlText;
|
||||||
|
bool m_insecure = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#include "useragent.h"
|
||||||
|
|
||||||
|
#include <QWebEngineClientHints>
|
||||||
|
#include <QWebEngineProfile>
|
||||||
|
#include <QtWebEngineCore/qtwebenginecoreglobal.h>
|
||||||
|
|
||||||
|
namespace useragent {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Gecko and WebKit versions can't come from the engine, so they are pinned
|
||||||
|
// here; bump them now and then so att doesn't look like an outdated browser.
|
||||||
|
constexpr auto kFirefoxVersion = "155.0";
|
||||||
|
constexpr auto kSafariVersion = "27.0";
|
||||||
|
|
||||||
|
// Chrome-based presets claim the engine's own Chromium major version, as
|
||||||
|
// qutebrowser does: a newer claim would make sites use features the engine
|
||||||
|
// lacks. Chrome's reduced UA string zeroes the minor parts.
|
||||||
|
QString chromeVersion()
|
||||||
|
{
|
||||||
|
return QString::fromLatin1(qWebEngineChromiumVersion()).section(QLatin1Char('.'), 0, 0)
|
||||||
|
+ QStringLiteral(".0.0.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// What Sec-CH-UA-* (and navigator.userAgentData) should say, so they don't
|
||||||
|
// contradict the User-Agent string. Chromium always sends the low-entropy ones
|
||||||
|
// (brands, platform, mobile), even with client hints disabled, so every preset
|
||||||
|
// sets a matching platform. Firefox and Safari send no hints at all, so for
|
||||||
|
// them the high-entropy ones are disabled.
|
||||||
|
struct Hints {
|
||||||
|
bool enabled;
|
||||||
|
QString brand; // "Google Chrome" etc., next to Chromium's own brands
|
||||||
|
QString platform;
|
||||||
|
QString platformVersion;
|
||||||
|
QString arch;
|
||||||
|
QString bitness;
|
||||||
|
bool mobile;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full version list for a brand: Chrome sends its own brand next to
|
||||||
|
// Chromium's (and a GREASE "Not A Brand"); an empty brand gives the engine's.
|
||||||
|
QVariantMap brands(const QString &brand)
|
||||||
|
{
|
||||||
|
const QString full = QString::fromLatin1(qWebEngineChromiumVersion());
|
||||||
|
QVariantMap list{{QStringLiteral("Chromium"), full},
|
||||||
|
{QStringLiteral("Not=A?Brand"), QStringLiteral("24.0.0.0")}};
|
||||||
|
if (!brand.isEmpty())
|
||||||
|
list.insert(brand, full);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
Preset preset;
|
||||||
|
Hints hints;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<Entry> buildEntries()
|
||||||
|
{
|
||||||
|
const QString chrome = chromeVersion();
|
||||||
|
const QString firefox = QString::fromLatin1(kFirefoxVersion);
|
||||||
|
const QString safari = QString::fromLatin1(kSafariVersion);
|
||||||
|
const auto chromeOn = [&](const QString &platform) {
|
||||||
|
return QStringLiteral("Mozilla/5.0 (%1) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/%2 Safari/537.36")
|
||||||
|
.arg(platform, chrome);
|
||||||
|
};
|
||||||
|
const auto firefoxOn = [&](const QString &platform) {
|
||||||
|
return QStringLiteral("Mozilla/5.0 (%1; rv:%2) Gecko/20100101 Firefox/%2")
|
||||||
|
.arg(platform, firefox);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
{{QStringLiteral("chrome-linux"), chromeOn(QStringLiteral("X11; Linux x86_64"))},
|
||||||
|
{true, QStringLiteral("Google Chrome"), QStringLiteral("Linux"), QString(),
|
||||||
|
QStringLiteral("x86"), QStringLiteral("64"), false}},
|
||||||
|
{{QStringLiteral("chrome-windows"),
|
||||||
|
chromeOn(QStringLiteral("Windows NT 10.0; Win64; x64"))},
|
||||||
|
{true, QStringLiteral("Google Chrome"), QStringLiteral("Windows"),
|
||||||
|
QStringLiteral("19.0.0"), QStringLiteral("x86"), QStringLiteral("64"), false}},
|
||||||
|
{{QStringLiteral("chrome-mac"),
|
||||||
|
chromeOn(QStringLiteral("Macintosh; Intel Mac OS X 10_15_7"))},
|
||||||
|
{true, QStringLiteral("Google Chrome"), QStringLiteral("macOS"),
|
||||||
|
QStringLiteral("15.0.0"), QStringLiteral("arm"), QStringLiteral("64"), false}},
|
||||||
|
{{QStringLiteral("chrome-android"),
|
||||||
|
QStringLiteral("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) "
|
||||||
|
"Chrome/%1 Mobile Safari/537.36")
|
||||||
|
.arg(chrome)},
|
||||||
|
{true, QStringLiteral("Google Chrome"), QStringLiteral("Android"),
|
||||||
|
QStringLiteral("15.0.0"), QString(), QString(), true}},
|
||||||
|
{{QStringLiteral("edge-windows"),
|
||||||
|
chromeOn(QStringLiteral("Windows NT 10.0; Win64; x64"))
|
||||||
|
+ QStringLiteral(" Edg/%1").arg(chrome)},
|
||||||
|
{true, QStringLiteral("Microsoft Edge"), QStringLiteral("Windows"),
|
||||||
|
QStringLiteral("19.0.0"), QStringLiteral("x86"), QStringLiteral("64"), false}},
|
||||||
|
{{QStringLiteral("firefox-linux"), firefoxOn(QStringLiteral("X11; Linux x86_64"))},
|
||||||
|
{false, {}, QStringLiteral("Linux"), {}, {}, {}, false}},
|
||||||
|
{{QStringLiteral("firefox-windows"),
|
||||||
|
firefoxOn(QStringLiteral("Windows NT 10.0; Win64; x64"))},
|
||||||
|
{false, {}, QStringLiteral("Windows"), {}, {}, {}, false}},
|
||||||
|
{{QStringLiteral("firefox-mac"),
|
||||||
|
firefoxOn(QStringLiteral("Macintosh; Intel Mac OS X 10.15"))},
|
||||||
|
{false, {}, QStringLiteral("macOS"), {}, {}, {}, false}},
|
||||||
|
{{QStringLiteral("safari-mac"),
|
||||||
|
QStringLiteral("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||||
|
"AppleWebKit/605.1.15 "
|
||||||
|
"(KHTML, like Gecko) Version/%1 Safari/605.1.15")
|
||||||
|
.arg(safari)},
|
||||||
|
{false, {}, QStringLiteral("macOS"), {}, {}, {}, false}},
|
||||||
|
{{QStringLiteral("safari-iphone"),
|
||||||
|
QStringLiteral("Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) "
|
||||||
|
"AppleWebKit/605.1.15 "
|
||||||
|
"(KHTML, like Gecko) Version/%1 Mobile/15E148 Safari/604.1")
|
||||||
|
.arg(safari)},
|
||||||
|
{false, {}, QStringLiteral("iOS"), {}, {}, {}, true}},
|
||||||
|
// QtWebEngine's own string, which names it and gets some sites to
|
||||||
|
// refuse or degrade service.
|
||||||
|
{{QStringLiteral("qtwebengine"),
|
||||||
|
QStringLiteral("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||||||
|
"(KHTML, like Gecko) "
|
||||||
|
"QtWebEngine/%1 Chrome/%2 Safari/537.36")
|
||||||
|
.arg(QString::fromLatin1(qVersion()),
|
||||||
|
QString::fromLatin1(qWebEngineChromiumVersion()))},
|
||||||
|
{true, {}, QStringLiteral("Linux"), QString(), QStringLiteral("x86"),
|
||||||
|
QStringLiteral("64"), false}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Entry> &entries()
|
||||||
|
{
|
||||||
|
static const QList<Entry> all = buildEntries();
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ¤tName()
|
||||||
|
{
|
||||||
|
static QString name;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QList<Preset> &presets()
|
||||||
|
{
|
||||||
|
static const QList<Preset> all = [] {
|
||||||
|
QList<Preset> list;
|
||||||
|
for (const Entry &entry : entries())
|
||||||
|
list.append(entry.preset);
|
||||||
|
return list;
|
||||||
|
}();
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool apply(QWebEngineProfile *profile, const QString &name)
|
||||||
|
{
|
||||||
|
for (const Entry &entry : entries()) {
|
||||||
|
if (entry.preset.name != name)
|
||||||
|
continue;
|
||||||
|
profile->setHttpUserAgent(entry.preset.userAgent);
|
||||||
|
QWebEngineClientHints *hints = profile->clientHints();
|
||||||
|
hints->setAllClientHintsEnabled(entry.hints.enabled);
|
||||||
|
hints->setPlatform(entry.hints.platform);
|
||||||
|
hints->setPlatformVersion(entry.hints.platformVersion);
|
||||||
|
hints->setArch(entry.hints.arch);
|
||||||
|
hints->setBitness(entry.hints.bitness);
|
||||||
|
hints->setIsMobile(entry.hints.mobile);
|
||||||
|
hints->setFullVersionList(brands(entry.hints.brand));
|
||||||
|
currentName() = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString current()
|
||||||
|
{
|
||||||
|
return currentName();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace useragent
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QWebEngineProfile;
|
||||||
|
|
||||||
|
// Named User-Agent presets for ":useragent", like a picker over qutebrowser's
|
||||||
|
// content.headers.user_agent. The choice is profile-wide, so it applies to
|
||||||
|
// every window, from each page's next load on.
|
||||||
|
namespace useragent {
|
||||||
|
|
||||||
|
struct Preset {
|
||||||
|
QString name; // what ":useragent" takes, e.g. "chrome-linux"
|
||||||
|
QString userAgent; // the header and navigator.userAgent
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every preset, in the order completion shows them.
|
||||||
|
const QList<Preset> &presets();
|
||||||
|
|
||||||
|
// Switches the profile to the named preset. Returns false, changing nothing,
|
||||||
|
// for an unknown name.
|
||||||
|
bool apply(QWebEngineProfile *profile, const QString &name);
|
||||||
|
|
||||||
|
// The name last applied, or empty before the first apply().
|
||||||
|
QString current();
|
||||||
|
|
||||||
|
} // namespace useragent
|
||||||
Reference in New Issue
Block a user