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
+186 -32
View File
@@ -2,33 +2,40 @@
#include "adblock.h"
#include "commandline.h"
#include "config.h"
#include "completionview.h"
#include "greasemonkey.h"
#include "hints.h"
#include "history.h"
#include "statusbar.h"
#include "urlutil.h"
#include "useragent.h"
#include <QApplication>
#include <QClipboard>
#include <QCursor>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <QWebEngineFullScreenRequest>
#include <QtWebEngineCore/qtwebenginecoreglobal.h>
#include <QWebEnginePage>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineSettings>
#include <QWebEngineView>
#include <QWheelEvent>
#include <QWidget>
namespace {
constexpr int kScrollStep = 60;
constexpr double kZoomStep = 0.1;
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,
// and the query.
@@ -38,6 +45,13 @@ const QRegularExpression &openCommand()
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
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);
setMode(Mode::Normal);
setZoom(config::kZoomDefault, false);
m_view->setUrl(startUrl);
focusView();
}
@@ -81,18 +96,29 @@ void BrowserWindow::setupUi()
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(m_view, 1);
layout->addWidget(m_completion);
layout->addWidget(m_status);
layout->addWidget(m_commandLine);
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();
}
void BrowserWindow::connectView()
{
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.
if (m_mode == Mode::Hint)
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)
{
if (m_mode == Mode::Hint && mode != Mode::Hint)
@@ -181,6 +212,9 @@ void BrowserWindow::updateStatusVisibility()
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)))
return QMainWindow::eventFilter(watched, event);
@@ -256,10 +290,9 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
}
if (sequence == QLatin1String("go") || sequence == QLatin1String("gO")) {
// Edit the current URL, to open it here or in a new window.
m_commandLine->openWith((sequence == QLatin1String("go") ? QStringLiteral(":open ")
: QStringLiteral(":open -w "))
+ m_view->url().toString());
setMode(Mode::Command);
openCommandLine((sequence == QLatin1String("go") ? QStringLiteral(":open ")
: QStringLiteral(":open -w "))
+ m_view->url().toString());
return true;
}
return true; // Swallow the unknown sequence rather than leaking it.
@@ -271,13 +304,11 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
}
if (text == QLatin1String("o")) {
m_commandLine->openWith(QStringLiteral(":open "));
setMode(Mode::Command);
openCommandLine(QStringLiteral(":open "));
return true;
}
if (text == QLatin1String("O")) {
m_commandLine->openWith(QStringLiteral(":open -w "));
setMode(Mode::Command);
openCommandLine(QStringLiteral(":open -w "));
return true;
}
if (text == QLatin1String("d")) {
@@ -285,8 +316,7 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
return true;
}
if (text == QLatin1String(":")) {
m_commandLine->openWith(QStringLiteral(":"));
setMode(Mode::Command);
openCommandLine(QStringLiteral(":"));
return true;
}
if (text == QLatin1String("i")) {
@@ -340,15 +370,15 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
return true;
}
if (text == QLatin1String("+")) {
m_view->setZoomFactor(m_view->zoomFactor() + kZoomStep);
zoomStep(1);
return true;
}
if (text == QLatin1String("-")) {
m_view->setZoomFactor(qMax(0.25, m_view->zoomFactor() - kZoomStep));
zoomStep(-1);
return true;
}
if (text == QLatin1String("=")) {
m_view->setZoomFactor(1.0);
setZoom(config::kZoomDefault);
return true;
}
@@ -467,6 +497,46 @@ void BrowserWindow::runCommand(const QString &command)
m_view->forward();
} else if (name == QLatin1String("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")) {
if (m_session.history->clear()) {
// 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()
{
const QRegularExpressionMatch match = openCommand().match(m_commandLine->text());
if (!match.hasMatch()) {
const QString text = m_commandLine->text();
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({});
return;
}
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
m_completion->setEntries(m_session.history->search(words, kCompletionLimit));
placeCompletion();
}
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)
{
if (!m_completion->isVisible())
return;
const QString url = m_completion->cycle(step);
if (url.isEmpty())
const QString choice = m_completion->cycle(step);
if (choice.isEmpty())
return;
// Keep whichever of ":o " / ":open " the user typed.
const QRegularExpressionMatch match = openCommand().match(m_commandLine->text());
// Keep the command as the user typed it (":o " or ":open ", ":ua " or
// ":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 ");
m_commandLine->setCommandText(prefix + url);
m_commandLine->setCommandText(prefix + choice);
}
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)
{
m_view->page()->runJavaScript(
QStringLiteral("window.scrollBy(%1, %2);").arg(dx).arg(dy));
// A pixel-precise wheel event, which is what a touchpad sends, rather than
// 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)
{
m_view->page()->runJavaScript(
QStringLiteral("window.scrollBy(0, window.innerHeight * %1);").arg(factor));
scrollBy(0, qRound(m_view->height() * factor));
}
void BrowserWindow::scrollToEnd(bool bottom)
{
m_view->page()->runJavaScript(
bottom ? QStringLiteral("window.scrollTo(0, document.body.scrollHeight);")
: QStringLiteral("window.scrollTo(0, 0);"));
// Far more than any page is tall; Chromium stops at the end.
constexpr int kFar = 10'000'000;
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()