From 8ac9d0a131257b82728488592e1111c4546e051a Mon Sep 17 00:00:00 2001 From: louib Date: Wed, 19 Dec 2018 23:10:46 -0500 Subject: [PATCH 1/7] Add create command to keepassxc-cli (#2540) * Add tests for CLI::Create --- src/cli/CMakeLists.txt | 1 + src/cli/Command.cpp | 2 + src/cli/Create.cpp | 175 ++++++++++++++++++++++++++++++++++++++++ src/cli/Create.h | 39 +++++++++ src/cli/keepassxc-cli.1 | 3 + tests/TestCli.cpp | 76 ++++++++++++++++- tests/TestCli.h | 1 + 7 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 src/cli/Create.cpp create mode 100644 src/cli/Create.h diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 37a085ad..43a22407 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -16,6 +16,7 @@ set(cli_SOURCES Add.cpp Clip.cpp + Create.cpp Command.cpp Diceware.cpp Edit.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index ff74089a..2d34770a 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -24,6 +24,7 @@ #include "Add.h" #include "Clip.h" +#include "Create.h" #include "Diceware.h" #include "Edit.h" #include "Estimate.h" @@ -69,6 +70,7 @@ void populateCommands() if (commands.isEmpty()) { commands.insert(QString("add"), new Add()); commands.insert(QString("clip"), new Clip()); + commands.insert(QString("create"), new Create()); commands.insert(QString("diceware"), new Diceware()); commands.insert(QString("edit"), new Edit()); commands.insert(QString("estimate"), new Estimate()); diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp new file mode 100644 index 00000000..9bea1f9e --- /dev/null +++ b/src/cli/Create.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include +#include +#include +#include + +#include "Create.h" +#include "Utils.h" + +#include "core/Database.h" + +#include "keys/CompositeKey.h" +#include "keys/Key.h" + +Create::Create() +{ + name = QString("create"); + description = QObject::tr("Create a new database."); +} + +Create::~Create() +{ +} + +/** + * Create a database file using the command line. A key file and/or + * password can be specified to encrypt the password. If none is + * specified the function will fail. + * + * If a key file is specified but it can't be loaded, the function will + * fail. + * + * If the database is being saved in a non existant directory, the + * function will fail. + * + * @return EXIT_SUCCESS on success, or EXIT_FAILURE on failure + */ +int Create::execute(const QStringList& arguments) +{ + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + + QCommandLineParser parser; + + parser.setApplicationDescription(description); + parser.addPositionalArgument("database", QObject::tr("Path of the database.")); + parser.addOption(Command::KeyFileOption); + + parser.addHelpOption(); + parser.process(arguments); + + const QStringList args = parser.positionalArguments(); + if (args.size() < 1) { + out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli create"); + return EXIT_FAILURE; + } + + QString databaseFilename = args.at(0); + if (QFileInfo::exists(databaseFilename)) { + err << QObject::tr("File %1 already exists.").arg(databaseFilename) << endl; + return EXIT_FAILURE; + } + + auto key = QSharedPointer::create(); + + auto password = getPasswordFromStdin(); + if (!password.isNull()) { + key->addKey(password); + } + + QSharedPointer fileKey; + if(parser.isSet(Command::KeyFileOption)) { + if (!loadFileKey(parser.value(Command::KeyFileOption), fileKey)) { + err << QObject::tr("Loading the key file failed") << endl; + return EXIT_FAILURE; + } + } + + if (!fileKey.isNull()) { + key->addKey(fileKey); + } + + if (key->isEmpty()) { + err << QObject::tr("No key is set. Aborting database creation.") << endl; + return EXIT_FAILURE; + } + + Database db; + db.setKey(key); + + QString errorMessage; + if (!db.save(databaseFilename, &errorMessage, true, false)) { + err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + + out << QObject::tr("Successfully created new database.") << endl; + return EXIT_SUCCESS; +} + +/** + * Read optional password from stdin. + * + * @return Pointer to the PasswordKey or null if passwordkey is skipped + * by user + */ +QSharedPointer Create::getPasswordFromStdin() +{ + QSharedPointer passwordKey; + QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); + + out << QObject::tr("Insert password to encrypt database (Press enter to leave blank): "); + out.flush(); + QString password = Utils::getPassword(); + + if (!password.isEmpty()) { + passwordKey = QSharedPointer(new PasswordKey(password)); + } + + return passwordKey; +} + +/** + * Load a key file from disk. When the path specified does not exist a + * new file will be generated. No folders will be generated so the parent + * folder of the specified file nees to exist + * + * If the key file cannot be loaded or created the function will fail. + * + * @param path Path to the key file to be loaded + * @param fileKey Resulting fileKey + * @return true if the key file was loaded succesfully + */ +bool Create::loadFileKey(QString path, QSharedPointer& fileKey) +{ + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + + QString error; + fileKey = QSharedPointer(new FileKey()); + + if (!QFileInfo::exists(path)) { + fileKey->create(path, &error); + + if (!error.isEmpty()) { + err << QObject::tr("Creating KeyFile %1 failed: %2").arg(path, error) << endl; + return false; + } + } + + if (!fileKey->load(path, &error)) { + err << QObject::tr("Loading KeyFile %1 failed: %2").arg(path, error) << endl; + return false; + } + + return true; +} diff --git a/src/cli/Create.h b/src/cli/Create.h new file mode 100644 index 00000000..9c14d37b --- /dev/null +++ b/src/cli/Create.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_CREATE_H +#define KEEPASSXC_CREATE_H + +#include "Command.h" + +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" + +class Create : public Command +{ +public: + Create(); + ~Create(); + int execute(const QStringList& arguments); + +private: + QSharedPointer getPasswordFromStdin(); + QSharedPointer getFileKeyFromStdin(); + bool loadFileKey(QString path, QSharedPointer& fileKey); +}; + +#endif // KEEPASSXC_CREATE_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index d7b0f9ad..22cf88a3 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -19,6 +19,9 @@ Adds a new entry to a database. A password can be generated (\fI-g\fP option), o .IP "clip [options] [timeout]" Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. +.IP "create [options] " +Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. + .IP "diceware [options]" Generate a random diceware passphrase. diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 430a3ba3..812e1616 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -34,6 +34,7 @@ #include "cli/Add.h" #include "cli/Clip.h" #include "cli/Command.h" +#include "cli/Create.h" #include "cli/Diceware.h" #include "cli/Edit.h" #include "cli/Estimate.h" @@ -137,9 +138,10 @@ QSharedPointer TestCli::readTestDatabase() const void TestCli::testCommand() { - QCOMPARE(Command::getCommands().size(), 12); + QCOMPARE(Command::getCommands().size(), 13); QVERIFY(Command::getCommand("add")); QVERIFY(Command::getCommand("clip")); + QVERIFY(Command::getCommand("create")); QVERIFY(Command::getCommand("diceware")); QVERIFY(Command::getCommand("edit")); QVERIFY(Command::getCommand("estimate")); @@ -274,7 +276,7 @@ void TestCli::testClip() // clang-format off QFuture future = QtConcurrent::run(&clipCmd, &Clip::execute, QStringList{"clip", m_dbFile->fileName(), "/Sample Entry", "1"}); // clang-format on - + QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString("Password"), 500); QTRY_COMPARE_WITH_TIMEOUT(clipboard->text(), QString(""), 1500); @@ -296,6 +298,76 @@ void TestCli::testClip() QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n")); } +void TestCli::testCreate() +{ + Create createCmd; + QVERIFY(!createCmd.name.isEmpty()); + QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name)); + + QScopedPointer testDir(new QTemporaryDir()); + + QString databaseFilename = testDir->path() + "testCreate1.kdbx"; + // Password + Utils::Test::setNextPassword("a"); + createCmd.execute({"create", databaseFilename}); + + m_stderrFile->reset(); + m_stdoutFile->reset(); + + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Insert password to encrypt database (Press enter to leave blank): \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); + + Utils::Test::setNextPassword("a"); + auto db = QSharedPointer(Utils::unlockDatabase(databaseFilename, "", Utils::DEVNULL)); + QVERIFY(db); + + // Should refuse to create the database if it already exists. + qint64 pos = m_stdoutFile->pos(); + qint64 errPos = m_stderrFile->pos(); + createCmd.execute({"create", databaseFilename}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + // Output should be empty when there is an error. + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QString errorMessage = QString("File " + databaseFilename + " already exists.\n"); + QCOMPARE(m_stderrFile->readAll(), errorMessage.toUtf8()); + + + // Testing with keyfile creation + QString databaseFilename2 = testDir->path() + "testCreate2.kdbx"; + QString keyfilePath = testDir->path() + "keyfile.txt"; + pos = m_stdoutFile->pos(); + errPos = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + createCmd.execute({"create", databaseFilename2, "-k", keyfilePath}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Insert password to encrypt database (Press enter to leave blank): \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); + + Utils::Test::setNextPassword("a"); + auto db2 = QSharedPointer(Utils::unlockDatabase(databaseFilename2, keyfilePath, Utils::DEVNULL)); + QVERIFY(db2); + + + // Testing with existing keyfile + QString databaseFilename3 = testDir->path() + "testCreate3.kdbx"; + pos = m_stdoutFile->pos(); + errPos = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + createCmd.execute({"create", databaseFilename3, "-k", keyfilePath}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Insert password to encrypt database (Press enter to leave blank): \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); + + Utils::Test::setNextPassword("a"); + auto db3 = QSharedPointer(Utils::unlockDatabase(databaseFilename3, keyfilePath, Utils::DEVNULL)); + QVERIFY(db3); +} + void TestCli::testDiceware() { Diceware dicewareCmd; diff --git a/tests/TestCli.h b/tests/TestCli.h index 63cd1f55..f3655e6c 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -43,6 +43,7 @@ private slots: void testCommand(); void testAdd(); void testClip(); + void testCreate(); void testDiceware(); void testEdit(); void testEstimate_data(); From 4d4c839afae2beb9da24ee5a5c6a3756a0d6b2c8 Mon Sep 17 00:00:00 2001 From: Kyle Kneitinger Date: Wed, 19 Dec 2018 20:14:11 -0800 Subject: [PATCH 2/7] Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up. --- src/browser/BrowserService.cpp | 85 +++++---- src/core/Bootstrap.cpp | 3 + src/gui/DatabaseTabWidget.cpp | 4 +- src/gui/DatabaseWidget.cpp | 84 +++++---- src/gui/EditWidgetIcons.cpp | 20 ++- src/gui/EditWidgetProperties.cpp | 15 +- src/gui/MessageBox.cpp | 165 +++++++++++++----- src/gui/MessageBox.h | 101 ++++++++--- src/gui/csvImport/CsvImportWidget.cpp | 4 +- .../DatabaseSettingsWidgetBrowser.cpp | 40 ++--- .../DatabaseSettingsWidgetMasterKey.cpp | 14 +- src/gui/entry/EditEntryWidget.cpp | 27 +-- src/gui/entry/EntryAttachmentsWidget.cpp | 32 ++-- tests/TestAutoType.cpp | 6 +- tests/gui/TestGui.cpp | 32 ++-- 15 files changed, 400 insertions(+), 232 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 6b85c786..bf42eab3 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -34,6 +34,7 @@ #include "core/Metadata.h" #include "core/PasswordGenerator.h" #include "gui/MainWindow.h" +#include "gui/MessageBox.h" const char BrowserService::KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; const char BrowserService::KEEPASSXCBROWSER_OLD_NAME[] = "keepassxc-browser Settings"; @@ -157,7 +158,7 @@ QString BrowserService::storeKey(const QString& key) } bool contains; - QMessageBox::StandardButton dialogResult = QMessageBox::No; + MessageBox::Button dialogResult = MessageBox::Cancel; do { QInputDialog keyDialog; @@ -180,14 +181,15 @@ QString BrowserService::storeKey(const QString& key) contains = db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); if (contains) { - dialogResult = QMessageBox::warning(nullptr, - tr("KeePassXC: Overwrite existing key?"), - tr("A shared encryption key with the name \"%1\" " - "already exists.\nDo you want to overwrite it?") - .arg(id), - QMessageBox::Yes | QMessageBox::No); + dialogResult = MessageBox::warning(nullptr, + tr("KeePassXC: Overwrite existing key?"), + tr("A shared encryption key with the name \"%1\" " + "already exists.\nDo you want to overwrite it?") + .arg(id), + MessageBox::Overwrite | MessageBox::Cancel, + MessageBox::Cancel); } - } while (contains && dialogResult == QMessageBox::No); + } while (contains && dialogResult == MessageBox::Cancel); db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key); return id; @@ -367,21 +369,17 @@ void BrowserService::updateEntry(const QString& id, if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { - int dialogResult = QMessageBox::No; + MessageBox::Button dialogResult = MessageBox::No; if (!browserSettings()->alwaysAllowUpdate()) { - QMessageBox msgBox; - msgBox.setWindowTitle(tr("KeePassXC: Update Entry")); - msgBox.setText(tr("Do you want to update the information in %1 - %2?").arg(QUrl(url).host(), username)); - msgBox.setStandardButtons(QMessageBox::Yes); - msgBox.addButton(QMessageBox::No); - msgBox.setDefaultButton(QMessageBox::No); - msgBox.setWindowFlags(Qt::WindowStaysOnTopHint); - msgBox.activateWindow(); - msgBox.raise(); - dialogResult = msgBox.exec(); + dialogResult = MessageBox::question(nullptr, + tr("KeePassXC: Update Entry"), + tr("Do you want to update the information in %1 - %2?") + .arg(QUrl(url).host(), username), + MessageBox::Save | MessageBox::Cancel, + MessageBox::Cancel); } - if (browserSettings()->alwaysAllowUpdate() || dialogResult == QMessageBox::Yes) { + if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) { entry->beginUpdate(); entry->setUsername(login); entry->setPassword(password); @@ -497,24 +495,24 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer curr progress.reset(); if (counter > 0) { - QMessageBox::information(nullptr, - tr("KeePassXC: Converted KeePassHTTP attributes"), - tr("Successfully converted attributes from %1 entry(s).\n" - "Moved %2 keys to custom data.", - "") - .arg(counter) - .arg(keyCounter), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully converted attributes from %1 entry(s).\n" + "Moved %2 keys to custom data.", + "") + .arg(counter) + .arg(keyCounter), + MessageBox::Ok); } else if (counter == 0 && keyCounter > 0) { - QMessageBox::information(nullptr, - tr("KeePassXC: Converted KeePassHTTP attributes"), - tr("Successfully moved %n keys to custom data.", "", keyCounter), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: Converted KeePassHTTP attributes"), + tr("Successfully moved %n keys to custom data.", "", keyCounter), + MessageBox::Ok); } else { - QMessageBox::information(nullptr, - tr("KeePassXC: No entry with KeePassHTTP attributes found!"), - tr("The active database does not contain an entry with KeePassHTTP attributes."), - QMessageBox::Ok); + MessageBox::information(nullptr, + tr("KeePassXC: No entry with KeePassHTTP attributes found!"), + tr("The active database does not contain an entry with KeePassHTTP attributes."), + MessageBox::Ok); } // Rename password groupName @@ -901,13 +899,14 @@ bool BrowserService::checkLegacySettings() return false; } - auto dialogResult = QMessageBox::warning(nullptr, - tr("KeePassXC: Legacy browser integration settings detected"), - tr("Legacy browser integration settings have been detected.\n" - "Do you want to upgrade the settings to the latest standard?\n" - "This is necessary to maintain compatibility with the browser plugin."), - QMessageBox::Yes | QMessageBox::No); - return dialogResult == QMessageBox::Yes; + auto dialogResult = MessageBox::warning(nullptr, + tr("KeePassXC: Legacy browser integration settings detected"), + tr("Legacy browser integration settings have been detected.\n" + "Do you want to upgrade the settings to the latest standard?\n" + "This is necessary to maintain compatibility with the browser plugin."), + MessageBox::Yes | MessageBox::No); + + return dialogResult == MessageBox::Yes; } void BrowserService::databaseLocked(DatabaseWidget* dbWidget) diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index a62cc5a9..2d8213b2 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -18,10 +18,12 @@ #include "Bootstrap.h" #include "core/Config.h" #include "core/Translator.h" +#include "gui/MessageBox.h" #ifdef Q_OS_WIN #include // for createWindowsDACL() #include // for Sleep(), SetDllDirectoryA(), SetSearchPathMode(), ... +#undef MessageBox #endif namespace Bootstrap @@ -56,6 +58,7 @@ namespace Bootstrap setupSearchPaths(); applyEarlyQNetworkAccessManagerWorkaround(); Translator::installTranslators(); + MessageBox::initializeButtonDefs(); #ifdef Q_OS_MACOS // Don't show menu icons on OSX diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index b45c5a8a..21c90cfa 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -98,8 +98,8 @@ QSharedPointer DatabaseTabWidget::execNewDatabaseWizard() tr("Database creation error"), tr("The created database has no key or KDF, refusing to save it.\n" "This is definitely a bug, please report it to the developers."), - QMessageBox::Ok, - QMessageBox::Ok); + MessageBox::Ok, + MessageBox::Ok); return {}; } diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 234f7e04..fb2d1a9a 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -445,6 +445,7 @@ void DatabaseWidget::deleteEntries() bool inRecycleBin = recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()); if (inRecycleBin || !m_db->metadata()->recycleBinEnabled()) { QString prompt; + refreshSearch(); if (selected.size() == 1) { prompt = tr("Do you really want to delete the entry \"%1\" for good?") .arg(selectedEntries.first()->title().toHtmlEscaped()); @@ -452,12 +453,16 @@ void DatabaseWidget::deleteEntries() prompt = tr("Do you really want to delete %n entry(s) for good?", "", selected.size()); } - QMessageBox::StandardButton result = MessageBox::question( - this, tr("Delete entry(s)?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); + auto answer = MessageBox::question(this, + tr("Delete entry(s)?", "", selected.size()), + prompt, + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); - if (result == QMessageBox::Yes) { + if (answer == MessageBox::Delete) { for (Entry* entry : asConst(selectedEntries)) { delete entry; + refreshSearch(); } refreshSearch(); } @@ -470,10 +475,13 @@ void DatabaseWidget::deleteEntries() prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", selected.size()); } - QMessageBox::StandardButton result = MessageBox::question( - this, tr("Move entry(s) to recycle bin?", "", selected.size()), prompt, QMessageBox::Yes | QMessageBox::No); + auto answer = MessageBox::question(this, + tr("Move entry(s) to recycle bin?", "", selected.size()), + prompt, + MessageBox::Move | MessageBox::Cancel, + MessageBox::Cancel); - if (result == QMessageBox::No) { + if (answer == MessageBox::Cancel) { return; } @@ -650,16 +658,27 @@ void DatabaseWidget::deleteGroup() bool isRecycleBin = recycleBin && (currentGroup == recycleBin); bool isRecycleBinSubgroup = recycleBin && currentGroup->findGroupByUuid(recycleBin->uuid()); if (inRecycleBin || isRecycleBin || isRecycleBinSubgroup || !m_db->metadata()->recycleBinEnabled()) { - QMessageBox::StandardButton result = MessageBox::question( - this, - tr("Delete group?"), - tr("Do you really want to delete the group \"%1\" for good?").arg(currentGroup->name().toHtmlEscaped()), - QMessageBox::Yes | QMessageBox::No); - if (result == QMessageBox::Yes) { + auto result = MessageBox::question(this, + tr("Delete group"), + tr("Do you really want to delete the group \"%1\" for good?") + .arg(currentGroup->name().toHtmlEscaped()), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Delete) { delete currentGroup; } } else { - m_db->recycleGroup(currentGroup); + auto result = MessageBox::question(this, + tr("Move group to recycle bin?"), + tr("Do you really want to move the group " + "\"%1\" to the recycle bin?") + .arg(currentGroup->name().toHtmlEscaped()), + MessageBox::Move | MessageBox::Cancel, + MessageBox::Cancel); + if (result == MessageBox::Move) { + m_db->recycleGroup(currentGroup); + } } } @@ -1127,8 +1146,8 @@ bool DatabaseWidget::lock() if (currentMode() == DatabaseWidget::Mode::EditMode) { auto result = MessageBox::question(this, tr("Lock Database?"), tr("You are editing an entry. Discard changes and lock anyway?"), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel); - if (result == QMessageBox::Cancel) { + MessageBox::Discard | MessageBox::Cancel, MessageBox::Cancel); + if (result == MessageBox::Cancel) { return false; } } @@ -1146,10 +1165,10 @@ bool DatabaseWidget::lock() msg = tr("Database was modified.\nSave changes?"); } auto result = MessageBox::question(this, tr("Save changes?"), msg, - QMessageBox::Yes | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Yes); - if (result == QMessageBox::Yes && !m_db->save(nullptr, false, false)) { + MessageBox::Save | MessageBox::Discard | MessageBox::Cancel, MessageBox::Save); + if (result == MessageBox::Save && !m_db->save(nullptr, false, false)) { return false; - } else if (result == QMessageBox::Cancel) { + } else if (result == MessageBox::Cancel) { return false; } } @@ -1240,9 +1259,9 @@ void DatabaseWidget::reloadDatabaseFile() auto result = MessageBox::question(this, tr("File has changed"), tr("The database file has changed. Do you want to load the changes?"), - QMessageBox::Yes | QMessageBox::No); + MessageBox::Yes | MessageBox::No); - if (result == QMessageBox::No) { + if (result == MessageBox::No) { // Notify everyone the database does not match the file m_db->markAsModified(); // Rewatch the database file @@ -1259,9 +1278,10 @@ void DatabaseWidget::reloadDatabaseFile() auto result = MessageBox::question(this, tr("Merge Request"), tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"), - QMessageBox::Yes | QMessageBox::No); + MessageBox::Merge | MessageBox::Cancel, + MessageBox::Merge); - if (result == QMessageBox::Yes) { + if (result == MessageBox::Merge) { // Merge the old database into the new one Merger merger(m_db.data(), db.data()); merger.merge(); @@ -1447,14 +1467,14 @@ bool DatabaseWidget::save(int attempt) if (attempt > 2 && useAtomicSaves) { // Saving failed 3 times, issue a warning and attempt to resolve - auto choice = MessageBox::question(this, + auto result = MessageBox::question(this, tr("Disable safe saves?"), tr("KeePassXC has failed to save the database multiple times. " "This is likely caused by file sync services holding a lock on " "the save file.\nDisable safe saves and try again?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes); - if (choice == QMessageBox::Yes) { + MessageBox::Disable | MessageBox::Cancel, + MessageBox::Disable); + if (result == MessageBox::Disable) { config()->set("UseAtomicSaves", false); return save(attempt + 1); } @@ -1529,13 +1549,13 @@ void DatabaseWidget::emptyRecycleBin() return; } - QMessageBox::StandardButton result = - MessageBox::question(this, - tr("Empty recycle bin?"), - tr("Are you sure you want to permanently delete everything from your recycle bin?"), - QMessageBox::Yes | QMessageBox::No); + auto result = MessageBox::question(this, + tr("Empty recycle bin?"), + tr("Are you sure you want to permanently delete everything from your recycle bin?"), + MessageBox::Empty | MessageBox::Cancel, + MessageBox::Cancel); - if (result == QMessageBox::Yes) { + if (result == MessageBox::Empty) { m_db->emptyRecycleBin(); refreshSearch(); } diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index abb65c00..6ac29825 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -416,16 +416,18 @@ void EditWidgetIcons::removeCustomIcon() int iconUseCount = entriesWithSameIcon.size() + groupsWithSameIcon.size(); if (iconUseCount > 0) { - QMessageBox::StandardButton ans = - MessageBox::question(this, - tr("Confirm Delete"), - tr("This icon is used by %n entry(s), and will be replaced " - "by the default icon. Are you sure you want to delete it?", - "", - iconUseCount), - QMessageBox::Yes | QMessageBox::No); - if (ans == QMessageBox::No) { + + auto result = MessageBox::question(this, + tr("Confirm Delete"), + tr("This icon is used by %n entry(s), and will be replaced " + "by the default icon. Are you sure you want to delete it?", + "", + iconUseCount), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Cancel) { // Early out, nothing is changed return; } else { diff --git a/src/gui/EditWidgetProperties.cpp b/src/gui/EditWidgetProperties.cpp index bc0f6e16..d89bf62b 100644 --- a/src/gui/EditWidgetProperties.cpp +++ b/src/gui/EditWidgetProperties.cpp @@ -68,13 +68,14 @@ const CustomData* EditWidgetProperties::customData() const void EditWidgetProperties::removeSelectedPluginData() { - if (QMessageBox::Yes - != MessageBox::question(this, - tr("Delete plugin data?"), - tr("Do you really want to delete the selected plugin data?\n" - "This may cause the affected plugins to malfunction."), - QMessageBox::Yes | QMessageBox::Cancel, - QMessageBox::Cancel)) { + auto result = MessageBox::question(this, + tr("Delete plugin data?"), + tr("Do you really want to delete the selected plugin data?\n" + "This may cause the affected plugins to malfunction."), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + if (result == MessageBox::Cancel) { return; } diff --git a/src/gui/MessageBox.cpp b/src/gui/MessageBox.cpp index 7aba6a2a..1460a6c7 100644 --- a/src/gui/MessageBox.cpp +++ b/src/gui/MessageBox.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2013 Felix Geyer + * Copyright (C) 2018 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,69 +18,139 @@ #include "MessageBox.h" -QMessageBox::StandardButton MessageBox::m_nextAnswer(QMessageBox::NoButton); +MessageBox::Button MessageBox::m_nextAnswer(MessageBox::NoButton); -QMessageBox::StandardButton MessageBox::critical(QWidget* parent, - const QString& title, - const QString& text, - QMessageBox::StandardButtons buttons, - QMessageBox::StandardButton defaultButton) +QMap +MessageBox::m_addedButtonLookup = + QMap(); + +QMap> +MessageBox::m_buttonDefs = + QMap>(); + +void MessageBox::initializeButtonDefs() { - if (m_nextAnswer == QMessageBox::NoButton) { - return QMessageBox::critical(parent, title, text, buttons, defaultButton); + m_buttonDefs = + QMap> + { + // Reimplementation of Qt StandardButtons + {Ok, {stdButtonText(QMessageBox::Ok), QMessageBox::ButtonRole::AcceptRole}}, + {Open, {stdButtonText(QMessageBox::Open), QMessageBox::ButtonRole::AcceptRole}}, + {Save, {stdButtonText(QMessageBox::Save), QMessageBox::ButtonRole::AcceptRole}}, + {Cancel, {stdButtonText(QMessageBox::Cancel), QMessageBox::ButtonRole::RejectRole}}, + {Close, {stdButtonText(QMessageBox::Close), QMessageBox::ButtonRole::RejectRole}}, + {Discard, {stdButtonText(QMessageBox::Discard), QMessageBox::ButtonRole::DestructiveRole}}, + {Apply, {stdButtonText(QMessageBox::Apply), QMessageBox::ButtonRole::ApplyRole}}, + {Reset, {stdButtonText(QMessageBox::Reset), QMessageBox::ButtonRole::ResetRole}}, + {RestoreDefaults, {stdButtonText(QMessageBox::RestoreDefaults), QMessageBox::ButtonRole::ResetRole}}, + {Help, {stdButtonText(QMessageBox::Help), QMessageBox::ButtonRole::HelpRole}}, + {SaveAll, {stdButtonText(QMessageBox::SaveAll), QMessageBox::ButtonRole::AcceptRole}}, + {Yes, {stdButtonText(QMessageBox::Yes), QMessageBox::ButtonRole::YesRole}}, + {YesToAll, {stdButtonText(QMessageBox::YesToAll), QMessageBox::ButtonRole::YesRole}}, + {No, {stdButtonText(QMessageBox::No), QMessageBox::ButtonRole::NoRole}}, + {NoToAll, {stdButtonText(QMessageBox::NoToAll), QMessageBox::ButtonRole::NoRole}}, + {Abort, {stdButtonText(QMessageBox::Abort), QMessageBox::ButtonRole::RejectRole}}, + {Retry, {stdButtonText(QMessageBox::Retry), QMessageBox::ButtonRole::AcceptRole}}, + {Ignore, {stdButtonText(QMessageBox::Ignore), QMessageBox::ButtonRole::AcceptRole}}, + + // KeePassXC Buttons + {Overwrite, {QMessageBox::tr("Overwrite"), QMessageBox::ButtonRole::AcceptRole}}, + {Delete, {QMessageBox::tr("Delete"), QMessageBox::ButtonRole::AcceptRole}}, + {Move, {QMessageBox::tr("Move"), QMessageBox::ButtonRole::AcceptRole}}, + {Empty, {QMessageBox::tr("Empty"), QMessageBox::ButtonRole::AcceptRole}}, + {Remove, {QMessageBox::tr("Remove"), QMessageBox::ButtonRole::AcceptRole}}, + {Skip, {QMessageBox::tr("Skip"), QMessageBox::ButtonRole::AcceptRole}}, + {Disable, {QMessageBox::tr("Disable"), QMessageBox::ButtonRole::AcceptRole}}, + {Merge, {QMessageBox::tr("Merge"), QMessageBox::ButtonRole::AcceptRole}}, + }; +} + +QString MessageBox::stdButtonText(QMessageBox::StandardButton button) +{ + QMessageBox buttonHost; + return buttonHost.addButton(button)->text(); +} + +MessageBox::Button MessageBox::messageBox(QWidget* parent, + QMessageBox::Icon icon, + const QString& title, + const QString& text, + MessageBox::Buttons buttons, + MessageBox::Button defaultButton) +{ + if (m_nextAnswer == MessageBox::NoButton) { + QMessageBox msgBox(parent); + msgBox.setIcon(icon); + msgBox.setWindowTitle(title); + msgBox.setText(text); + + for (uint64_t b = First; b <= Last; b <<= 1) { + if (b & buttons) { + QString text = m_buttonDefs[static_cast