Cockatrice/oracle/src/pagetemplates.cpp
Bryce! 3a656c9c4a
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / vcpkg baseline (push) Waiting to run
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Preserve installed card databases when Oracle refresh fails (#7739)
* Preserve installed card databases when Oracle refresh fails

* Fix the Oracle refresh verdict and cover the parser write path

A background refresh that already installed cards.xml exited 1 when the
token save afterwards failed, so the client kept its old in-memory
database and reported the update as failed. Return the same verdict
main() already computes: a reject after the database was written is a
successful update.

Cover the writer Oracle actually uses: add a short-write death test for
CockatriceXml4Parser::saveToFile, which reverted to QFile would truncate
an installed database without any test noticing.

Route a failed background token download through reject() instead of a
modal box on a hidden wizard, which left the client waiting with no
timeout and blocked later updates. Test the first non-space byte in
place rather than copying the payload, and report an import refusal with
the importer's own message.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 17:33:49 +01:00

278 lines
8.2 KiB
C++

#include "pagetemplates.h"
#include "database_writer.h"
#include "oraclewizard.h"
#include <QCheckBox>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QIODevice>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QObject>
#include <QProgressBar>
#include <QPushButton>
#include <QRadioButton>
#include <QSettings>
#include <QSharedPointer>
#include <QUrl>
#include <QVariant>
#include <qlogging.h>
#include <qnamespace.h>
class QWidget;
SimpleDownloadFilePage::SimpleDownloadFilePage(QWidget *parent) : OracleWizardPage(parent)
{
urlRadioButton = new QRadioButton(this);
fileRadioButton = new QRadioButton(this);
urlLineEdit = new QLineEdit(this);
fileLineEdit = new QLineEdit(this);
progressLabel = new QLabel(this);
progressBar = new QProgressBar(this);
urlRadioButton->setChecked(true);
urlButton = new QPushButton(this);
connect(urlButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actRestoreDefaultUrl);
fileButton = new QPushButton(this);
connect(fileButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actLoadCardFile);
defaultPathCheckBox = new QCheckBox(this);
pathLabel = new QLabel(this);
pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *layout = new QGridLayout(this);
layout->addWidget(urlRadioButton, 0, 0);
layout->addWidget(urlLineEdit, 0, 1);
layout->addWidget(urlButton, 1, 1, Qt::AlignRight);
layout->addWidget(fileRadioButton, 2, 0);
layout->addWidget(fileLineEdit, 2, 1);
layout->addWidget(fileButton, 3, 1, Qt::AlignRight);
layout->addWidget(pathLabel, 4, 0, 1, 2);
layout->addWidget(defaultPathCheckBox, 5, 0, 1, 2);
layout->addWidget(progressLabel, 6, 0);
layout->addWidget(progressBar, 6, 1);
setLayout(layout);
}
void SimpleDownloadFilePage::initializePage()
{
// get custom url from settings if any; otherwise use default url
urlLineEdit->setText(wizard()->settings->value(getCustomUrlSettingsKey(), getDefaultUrl()).toString());
progressLabel->hide();
progressBar->hide();
}
void SimpleDownloadFilePage::actRestoreDefaultUrl()
{
urlLineEdit->setText(getDefaultUrl());
}
void SimpleDownloadFilePage::actLoadCardFile()
{
QFileDialog dialog(this, tr("Load %1 file").arg(getFilePromptName()));
dialog.setFileMode(QFileDialog::ExistingFile);
QString extensions = "*.json *.xml";
#ifdef HAS_ZLIB
extensions += " *.zip";
#endif
#ifdef HAS_LZMA
extensions += " *.xz";
#endif
dialog.setNameFilter(tr("%1 file (%1)").arg(getFilePromptName(), extensions));
if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) {
dialog.selectFile(fileLineEdit->text());
}
if (!dialog.exec()) {
return;
}
fileLineEdit->setText(dialog.selectedFiles().at(0));
}
bool SimpleDownloadFilePage::validatePage()
{
// if data has already been downloaded, pass directly to the "save" step
if (!downloadData.isEmpty()) {
if (saveToFile()) {
return true;
} else {
wizard()->enableButtons();
return false;
}
}
// else, try to import sets
if (urlRadioButton->isChecked()) {
QUrl url = QUrl::fromUserInput(urlLineEdit->text());
if (!url.isValid()) {
QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString());
return false;
}
progressLabel->setText(tr("Downloading (0 MB)"));
// show an infinite progressbar
progressBar->setMaximum(0);
progressBar->setMinimum(0);
progressBar->setValue(0);
progressLabel->show();
progressBar->show();
wizard()->disableButtons();
downloadFile(url);
} else if (fileRadioButton->isChecked()) {
QFile cardFile(fileLineEdit->text());
if (!cardFile.exists()) {
QMessageBox::critical(this, tr("Error"), tr("Please choose a file."));
return false;
}
if (!cardFile.open(QIODevice::ReadOnly)) {
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text()));
return false;
}
downloadData = cardFile.readAll();
wizard()->next();
}
return false;
}
void SimpleDownloadFilePage::downloadFile(QUrl url)
{
QNetworkReply *reply = wizard()->nam->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, &SimpleDownloadFilePage::actDownloadFinished);
connect(reply, &QNetworkReply::downloadProgress, this, &SimpleDownloadFilePage::actDownloadProgress);
}
void SimpleDownloadFilePage::actDownloadProgress(qint64 received, qint64 total)
{
if (total > 0) {
progressBar->setMaximum(static_cast<int>(total));
progressBar->setValue(static_cast<int>(received));
}
progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024)));
}
void SimpleDownloadFilePage::actDownloadFinished()
{
// check for a reply
auto *reply = dynamic_cast<QNetworkReply *>(sender());
if (wizard()->wasCancelled()) {
reply->deleteLater();
return;
}
QNetworkReply::NetworkError errorCode = reply->error();
if (errorCode != QNetworkReply::NoError) {
const QString message = tr("Network error: %1.").arg(reply->errorString());
reply->deleteLater();
// A hidden wizard cannot show a modal box: without an explicit stop it would sit
// here forever and the hosting client would wait with no timeout.
if (wizard()->backgroundMode) {
qWarning() << message;
wizard()->reject();
return;
}
QMessageBox::critical(this, tr("Error"), message);
wizard()->enableButtons();
return;
}
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302) {
QUrl redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
qDebug() << "following redirect url:" << redirectUrl.toString();
downloadFile(redirectUrl);
reply->deleteLater();
return;
}
// save downloaded file url, but only if the user customized it and download was successful
if (urlLineEdit->text() != getDefaultUrl()) {
wizard()->settings->setValue(getCustomUrlSettingsKey(), urlLineEdit->text());
} else {
wizard()->settings->remove(getCustomUrlSettingsKey());
}
downloadData = reply->readAll();
reply->deleteLater();
wizard()->enableButtons();
progressLabel->hide();
progressBar->hide();
wizard()->next();
}
bool SimpleDownloadFilePage::saveToFile()
{
const auto failSave = [this](const QString &message) {
qWarning() << message;
if (wizard()->backgroundMode) {
wizard()->reject();
} else {
QMessageBox::critical(this, tr("Error"), message);
}
return false;
};
QString defaultPath = getDefaultSavePath();
QString windowName = getWindowTitle();
QString fileType = getFileType();
QString fileName;
if (defaultPathCheckBox->isChecked()) {
fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType);
} else {
fileName = defaultPath;
}
if (fileName.isEmpty()) {
return false;
}
QFileInfo fi(fileName);
QDir fileDir(fi.path());
if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
return failSave(tr("The database directory could not be created: %1").arg(fileDir.absolutePath()));
}
if (!internalSaveToFile(fileName)) {
return failSave(tr("The file could not be saved to %1").arg(fileName));
}
// clean saved downloadData
downloadData = QByteArray();
wizard()->markDatabaseSaved();
return true;
}
bool SimpleDownloadFilePage::internalSaveToFile(const QString &fileName)
{
QString error;
if (!OracleDatabase::saveXml(fileName, downloadData, &error)) {
qWarning() << error;
return false;
}
return true;
}