Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ BUILD_DIR="${ROOT_DIR}/build"

mkdir -p "${BUILD_DIR}"
pushd "${BUILD_DIR}" >/dev/null
qmake6 ../seb-linux-qt.pro
CONFIG="debug"
for arg in "$@"; do
if [ "$arg" == "-r" ] || [ "$arg" == "--release" ]; then
CONFIG="release"
break
fi
done
Comment thread
incomplete-tree marked this conversation as resolved.

qmake6 CONFIG+="${CONFIG}" ../seb-linux-qt.pro
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/build.sh | head -40

Repository: Jvr2022/seb-linux

Length of output: 697


🏁 Script executed:

# Search for any documentation or comments in the repo about build configuration
rg -i "config|debug|release" scripts/ -A 2 -B 2

Repository: Jvr2022/seb-linux

Length of output: 675


🌐 Web query:

qmake6 CONFIG mode changes same build directory object reuse detection

💡 Result:

qmake (including qmake6 in Qt6) does not have a specific CONFIG option to control object file reuse detection when changing CONFIG modes (like switching between debug/release) in the same build directory. qmake generates dependency information based on source files, headers (via DEPENDPATH), and Makefile regeneration triggers, but CONFIG changes primarily affect which Makefile is used (e.g., Makefile.Debug vs Makefile.Release when CONFIG += debug_and_release). Key behaviors: - When using CONFIG += debug_and_release (default on Windows, optional elsewhere), qmake generates separate Makefiles for each mode: Makefile, Makefile.Debug, Makefile.Release. Building 'debug' uses Makefile.Debug (which builds objects with debug flags), 'release' uses Makefile.Release (release flags). Objects are typically placed in mode-specific directories if configured via OBJECTS_DIR in CONFIG(debug,debug|release) scopes (e.g., OBJECTS_DIR = .obj/debug). Thus, objects are not shared/reused across modes due to different compiler flags and usually separate paths. - In the same build directory without debug_and_release, changing CONFIG (e.g., adding/removing 'debug') invalidates the Makefile, forcing qmake re-run and full rebuild (no incremental object reuse across config changes). - No evidence of qmake6-specific changes to this behavior; Qt6 docs confirm qmake remains supported for apps with same core logic. - To avoid rebuilds on CONFIG changes, use separate build directories per config (shadow builds, recommended). For object reuse across subprojects, object files are not automatically shared; each subdir rebuilds unless using static libs. - Use 'qmake -nodepend' to disable dependency generation (not recommended, breaks incremental builds). No official CONFIG like 'object_reuse_detection' exists. Best practice: separate OBJECTS_DIR per config, separate build dirs.

Citations:


🏁 Script executed:

find . -name "seb-linux-qt.pro" -o -name "*.pro" | head -5

Repository: Jvr2022/seb-linux

Length of output: 151


🏁 Script executed:

cat -n seb-linux-qt.pro | head -50

Repository: Jvr2022/seb-linux

Length of output: 2506


🏁 Script executed:

cat seb-linux-qt.pro | grep -i "debug_and_release\|CONFIG.*debug\|CONFIG.*release\|OBJECTS_DIR.*CONFIG"

Repository: Jvr2022/seb-linux

Length of output: 43


🏁 Script executed:

wc -l seb-linux-qt.pro && tail -50 seb-linux-qt.pro

Repository: Jvr2022/seb-linux

Length of output: 2692


🏁 Script executed:

grep -n "CONFIG\|debug_and_release" seb-linux-qt.pro

Repository: Jvr2022/seb-linux

Length of output: 120


🏁 Script executed:

grep -n "include\|qt_module" seb-linux-qt.pro | head -20

Repository: Jvr2022/seb-linux

Length of output: 43


🏁 Script executed:

# Check if there are any cache/state files generated by qmake that might track CONFIG
find . -name ".qmake*" -o -name "*.stash" -o -name "Makefile*" | grep -v ".git" | head -20

Repository: Jvr2022/seb-linux

Length of output: 43


🌐 Web query:

Qt6 qmake CONFIG change detection same build directory rebuild behavior

💡 Result:

In Qt6, qmake does not automatically detect changes to the CONFIG variable (or other .pro file changes) in the same build directory during incremental builds with make/nmake. The generated Makefile lacks a dependency on the .pro file itself, so changes to CONFIG (e.g., adding debug, release, or custom features) do not trigger qmake regeneration or rebuilds of affected targets. To apply CONFIG changes: - Manually run qmake in the build directory to regenerate the Makefile. - In Qt Creator: Right-click project > Run qmake, or use Build > Run qmake, or delete Makefile(s) to force regeneration. - Use 'make clean' followed by 'make', but this performs a full rebuild. This behavior persists from Qt5 to Qt6 as qmake remains supported for applications. Official docs confirm qmake usage but note CMake preference for Qt6 builds. Qt Creator may not always auto-run qmake on .pro changes (known longstanding issue). Shadow builds (separate build dir) are recommended to avoid source pollution, but the issue remains for .pro changes.

Citations:


🏁 Script executed:

# Check what happens when qmake runs with CONFIG changes
# Show the build.sh logic again for clarity
cat -n scripts/build.sh

Repository: Jvr2022/seb-linux

Length of output: 697


Use separate build directories or force clean rebuild when CONFIG changes.

Line 17 reconfigures qmake6 in the same ${ROOT_DIR}/build directory for both debug and release modes. Qt6 qmake does not track CONFIG variable changes in the Makefile, so switching from debug to release (or vice versa) will reuse object files compiled with the previous configuration's compiler flags. This results in binaries with mixed debug/release code and unintended debug overhead.

Solutions: (1) Use separate build directories per mode (e.g., build/debug, build/release), or (2) Detect CONFIG changes and perform make clean before rebuilding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/build.sh` at line 17, The qmake6 invocation reuses the same
${ROOT_DIR}/build directory regardless of CONFIG causing mixed debug/release
objects; modify the build.sh logic around the qmake6 CONFIG+="${CONFIG}"
../seb-linux-qt.pro call to either (A) select a per-configuration build
directory (e.g., use BUILD_DIR="${ROOT_DIR}/build/${CONFIG}" and run qmake6 from
there) or (B) detect when the previous CONFIG differs from the current CONFIG
and run a clean (make clean) before re-running qmake6, ensuring the build
directory is created/changed appropriately and old object files are removed when
CONFIG changes.

make -j"$(nproc)"
popd >/dev/null

echo "Build output: ${BUILD_DIR}/bin/safe-exam-browser"
echo "Build output: ${BUILD_DIR}/bin/safe-exam-browser (mode: ${CONFIG})"
21 changes: 21 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <QLineEdit>
#include <QTextStream>
#include <QUrl>
#include <QFileInfo>

namespace {

Expand All @@ -20,6 +21,14 @@ QString findConfigPath(int argc, char *argv[])
if ((argument == QStringLiteral("--config") || argument == QStringLiteral("-c")) && index + 1 < argc) {
return QString::fromLocal8Bit(argv[index + 1]);
}
if (argument == QStringLiteral("--url") || argument == QStringLiteral("-u")
#ifdef QT_DEBUG
|| argument == QStringLiteral("--inject") || argument == QStringLiteral("-i")
#endif
) {
++index;
continue;
}
if (!argument.startsWith('-')) {
return argument;
}
Expand Down Expand Up @@ -85,6 +94,12 @@ void applyCommandLineOverrides(const QCommandLineParser &parser, seb::SebSetting
if (parser.isSet("disable-quit")) {
settings.security.allowTermination = false;
}

#ifdef QT_DEBUG
if (parser.isSet("inject")) {
settings.browser.injectedScript = QFileInfo(parser.value("inject")).absoluteFilePath();
}
Comment thread
incomplete-tree marked this conversation as resolved.
#endif
}

} // namespace
Expand Down Expand Up @@ -134,6 +149,12 @@ int main(int argc, char *argv[])
parser.addOption(QCommandLineOption(
QStringLiteral("disable-quit"),
QStringLiteral("Disable manual termination even if the configuration allows it.")));
#ifdef QT_DEBUG
parser.addOption(QCommandLineOption(
QStringList{QStringLiteral("i"), QStringLiteral("inject")},
QStringLiteral("Inject a JavaScript file into each page."),
QStringLiteral("file")));
#endif

parser.process(app);

Expand Down
92 changes: 91 additions & 1 deletion src/seb_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,78 @@
#include <QApplication>
#include <QAuthenticator>
#include <QCryptographicHash>
#include <QDebug>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QInputDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocale>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTextStream>
#include <QUrl>
#include <QWebEngineCookieStore>
#include <QWebEngineDownloadRequest>
#include <QWebEngineProfile>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
#include <QWebEngineSettings>
#include <QTimer>

namespace {

QString getCachedSebVersion()
{
static QString cachedVersion;
if (!cachedVersion.isEmpty()) {
return cachedVersion;
}

cachedVersion = QStringLiteral("3.10.1");

QNetworkAccessManager manager;
QNetworkRequest request(QUrl(QStringLiteral("https://api.github.com/repos/SafeExamBrowser/seb-win-refactoring/releases/latest")));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("SEB Linux Qt/") + QApplication::applicationVersion());

QEventLoop loop;
QNetworkReply *reply = manager.get(request);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QTimer::singleShot(5000, &loop, &QEventLoop::quit);
loop.exec();

if (!reply->isFinished()) {
reply->abort();
}

if (reply->error() == QNetworkReply::NoError) {
const QByteArray responseData = reply->readAll();
QJsonDocument json = QJsonDocument::fromJson(responseData);
if (json.isObject()) {
QString tag = json.object().value(QStringLiteral("tag_name")).toString();
if (!tag.isEmpty()) {
if (tag.startsWith(QLatin1Char('v'), Qt::CaseInsensitive)) {
tag = tag.mid(1);
}
cachedVersion = tag;
}
}
}
reply->deleteLater();

return cachedVersion;
}
Comment thread
incomplete-tree marked this conversation as resolved.

} // namespace

SebSession::SebSession(const seb::SebSettings &settings, ResourceOpener opener, QObject *parent)
: QObject(parent)
Expand Down Expand Up @@ -61,6 +120,24 @@ SebSession::SebSession(const seb::SebSettings &settings, ResourceOpener opener,
profile_->setSpellCheckLanguages(QStringList{QLocale::system().bcp47Name()});
profile_->setHttpUserAgent(buildUserAgent());

#ifdef QT_DEBUG
if (!settings_.browser.injectedScript.isEmpty()) {
QFile scriptFile(settings_.browser.injectedScript);
if (scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QWebEngineScript script;
script.setName(QStringLiteral("InjectScript"));
QTextStream in(&scriptFile);
script.setSourceCode(in.readAll());
script.setInjectionPoint(QWebEngineScript::DocumentReady);
script.setWorldId(QWebEngineScript::UserWorld);
script.setRunsOnSubFrames(true);
profile_->scripts()->insert(script);
} else {
qWarning() << "Failed to open injection script:" << scriptFile.fileName() << scriptFile.errorString();
}
}
Comment thread
incomplete-tree marked this conversation as resolved.
#endif

interceptor_.reset(new seb::browser::RequestInterceptor(settings_, this));
profile_->setUrlRequestInterceptor(interceptor_.data());

Expand Down Expand Up @@ -361,11 +438,24 @@ void SebSession::handleDownloadRequested(QWebEngineDownloadRequest *download)

QString SebSession::buildUserAgent() const
{
QString agent = profile_->httpUserAgent();
QString agent;

if (settings_.browser.useCustomUserAgent && !settings_.browser.customUserAgent.isEmpty()) {
agent = settings_.browser.customUserAgent.trimmed();
} else {
Comment on lines 443 to +445
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trim before deciding whether to honor customUserAgent.

Line 443 only checks isEmpty(). A whitespace-only customUserAgent therefore skips the default UA, then Line 444 trims it to empty and the header degrades to just SEB/... instead of a valid browser UA.

🛠️ Proposed fix
-    if (settings_.browser.useCustomUserAgent && !settings_.browser.customUserAgent.isEmpty()) {
-        agent = settings_.browser.customUserAgent.trimmed();
+    const QString customAgent = settings_.browser.customUserAgent.trimmed();
+    if (settings_.browser.useCustomUserAgent && !customAgent.isEmpty()) {
+        agent = customAgent;
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (settings_.browser.useCustomUserAgent && !settings_.browser.customUserAgent.isEmpty()) {
agent = settings_.browser.customUserAgent.trimmed();
} else {
const QString customAgent = settings_.browser.customUserAgent.trimmed();
if (settings_.browser.useCustomUserAgent && !customAgent.isEmpty()) {
agent = customAgent;
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/seb_session.cpp` around lines 443 - 445, Trim
settings_.browser.customUserAgent before deciding to use it: compute a trimmed
string (reuse the local variable agent or a temporary) and test
trimmed.isEmpty() together with settings_.browser.useCustomUserAgent so
whitespace-only values fall back to the default; if non-empty after trimming,
assign the trimmed value to agent (instead of trimming only after the check).

QString defaultAgent = profile_->httpUserAgent();
QRegularExpression regex(QStringLiteral("Chrome/([0-9.]+)"));
QRegularExpressionMatch match = regex.match(defaultAgent);
QString chromeVersion = match.hasMatch() ? match.captured(1) : QStringLiteral("110.0.0.0");

agent = QStringLiteral("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/") + chromeVersion;
}

const QString sebVersion = QStringLiteral("SEB/") + getCachedSebVersion();
if (!agent.endsWith(' ')) {
agent += QLatin1Char(' ');
}
agent += sebVersion;

if (!settings_.browser.userAgentSuffix.trimmed().isEmpty()) {
if (!agent.endsWith(' ')) {
Expand Down
1 change: 1 addition & 0 deletions src/seb_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ struct BrowserSettings
QString startUrl;
QString startUrlQuery;
QString userAgentSuffix;
QString injectedScript;
bool allowConfigurationDownloads = true;
bool allowCustomDownAndUploadLocation = false;
bool allowDownloads = true;
Expand Down