diff --git a/CMakeLists.txt b/CMakeLists.txt index 32f7611ab9..643e2f093c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ endif (CCACHE_FOUND) # Support Visual Studio Code include(CMakeToolsHelpers OPTIONAL) +include(FeatureSummary) include(CheckCCompilerFlag) include(CheckCXXCompilerFlag) @@ -465,8 +466,6 @@ endif() include_directories(SYSTEM ${GCRYPT_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}) -include(FeatureSummary) - add_subdirectory(src) add_subdirectory(share) if(WITH_TESTS) diff --git a/COPYING b/COPYING index 9bfd33539a..fe7d02f3eb 100644 --- a/COPYING +++ b/COPYING @@ -248,3 +248,8 @@ Comment: from Freedesktop.org website Files: share/icons/application/32x32/actions/statistics.png Copyright: Icon made by Freepik from https://www.flaticon.com/free-icon/bars-chart_265733 + +Files: share/icons/application/scalable/actions/object-locked.svg + share/icons/application/scalable/actions/object-unlocked.svg +License: LGPL-3 +Comment: from Breeze icon theme (https://github.com/KDE/breeze-icons) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index bcc97efaea..2be6b198ac 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -117,7 +117,7 @@ Displays the program version. .IP "-d, --dry-run " Prints the changes detected by the merge operation without making any changes to the database. -.IP "-f, --key-file-from " +.IP "--key-file-from " Sets the path of the key file for the second database. .IP "--no-password-from" diff --git a/share/icons/application/scalable/actions/object-locked.svg b/share/icons/application/scalable/actions/object-locked.svg new file mode 100644 index 0000000000..090e038c0b --- /dev/null +++ b/share/icons/application/scalable/actions/object-locked.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/share/icons/application/scalable/actions/object-unlocked.svg b/share/icons/application/scalable/actions/object-unlocked.svg new file mode 100644 index 0000000000..f6c53e5816 --- /dev/null +++ b/share/icons/application/scalable/actions/object-unlocked.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7ad8739c6d..6b3d9abfab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -122,6 +122,7 @@ set(keepassx_SOURCES gui/TotpDialog.cpp gui/TotpExportSettingsDialog.cpp gui/DatabaseOpenDialog.cpp + gui/URLEdit.cpp gui/WelcomeWidget.cpp gui/csvImport/CsvImportWidget.cpp gui/csvImport/CsvImportWizard.cpp diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index 20b2fc975f..fec5b985a8 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -42,7 +42,7 @@ QJsonObject BrowserAction::readResponse(const QJsonObject& json) bool triggerUnlock = false; const QString trigger = json.value("triggerUnlock").toString(); - if (!trigger.isEmpty() && trigger.compare("true", Qt::CaseSensitive) == 0) { + if (!trigger.isEmpty() && trigger.compare(TRUE_STR, Qt::CaseSensitive) == 0) { triggerUnlock = true; } @@ -268,7 +268,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin const QString id = decrypted.value("id").toString(); const QString submit = decrypted.value("submitUrl").toString(); const QString auth = decrypted.value("httpAuth").toString(); - const bool httpAuth = auth.compare("true", Qt::CaseSensitive) == 0 ? true : false; + const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0 ? true : false; const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, "", keyList, httpAuth); if (users.isEmpty()) { @@ -469,7 +469,7 @@ QJsonObject BrowserAction::buildMessage(const QString& nonce) const { QJsonObject message; message["version"] = KEEPASSXC_VERSION; - message["success"] = "true"; + message["success"] = TRUE_STR; message["nonce"] = nonce; return message; } diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp index a5bb921da5..56c6cceb83 100644 --- a/src/browser/BrowserOptionDialog.cpp +++ b/src/browser/BrowserOptionDialog.cpp @@ -68,6 +68,10 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) connect(m_ui->useCustomProxy, SIGNAL(toggled(bool)), m_ui->customProxyLocationBrowseButton, SLOT(setEnabled(bool))); connect(m_ui->customProxyLocationBrowseButton, SIGNAL(clicked()), this, SLOT(showProxyLocationFileDialog())); +#ifndef Q_OS_LINUX + m_ui->snapWarningLabel->setVisible(false); +#endif + #ifdef Q_OS_WIN // Brave uses Chrome's registry settings m_ui->braveSupport->setHidden(true); diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui index 638c400aa3..84fc5bdbf6 100755 --- a/src/browser/BrowserOptionDialog.ui +++ b/src/browser/BrowserOptionDialog.ui @@ -60,7 +60,7 @@ - + Browsers installed as snaps are currently not supported. diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index c00229fd8e..63860b58da 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -54,6 +54,7 @@ static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwo // Extra entry related options saved in custom data const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit"); const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry"); +const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth"); // Multiple URL's const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL"); @@ -380,9 +381,14 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, // Check entries for authorization QList pwEntriesToConfirm; QList pwEntries; - for (auto* entry : searchEntries(url, keyList)) { + for (auto* entry : searchEntries(url, submitUrl, keyList)) { if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY) - && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == "true") { + && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR) { + continue; + } + + if (!httpAuth && entry->customData()->contains(BrowserService::OPTION_ONLY_HTTP_AUTH) + && entry->customData()->value(BrowserService::OPTION_ONLY_HTTP_AUTH) == TRUE_STR) { continue; } @@ -583,7 +589,7 @@ BrowserService::ReturnValue BrowserService::updateEntry(const QString& id, } QList -BrowserService::searchEntries(const QSharedPointer& db, const QString& hostname, const QString& url) +BrowserService::searchEntries(const QSharedPointer& db, const QString& url, const QString& submitUrl) { QList entries; auto* rootGroup = db->rootGroup(); @@ -601,19 +607,15 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& continue; } - auto domain = baseDomain(hostname); - // Search for additional URL's starting with KP2A_URL - if (entry->attributes()->keys().contains(ADDITIONAL_URL)) { - for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), domain, url)) { - entries.append(entry); - continue; - } + for (const auto& key : entry->attributes()->keys()) { + if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), url, submitUrl)) { + entries.append(entry); + continue; } } - if (!handleURL(entry->url(), domain, url)) { + if (!handleURL(entry->url(), url, submitUrl)) { continue; } @@ -624,7 +626,7 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& return entries; } -QList BrowserService::searchEntries(const QString& url, const StringPairList& keyList) +QList BrowserService::searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList) { // Check if database is connected with KeePassXC-Browser auto databaseConnected = [&](const QSharedPointer& db) { @@ -661,7 +663,7 @@ QList BrowserService::searchEntries(const QString& url, const StringPair QList entries; do { for (const auto& db : databases) { - entries << searchEntries(db, hostname, url); + entries << searchEntries(db, url, submitUrl); } } while (entries.isEmpty() && removeFirstDomain(hostname)); @@ -854,7 +856,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry) } if (entry->isExpired()) { - res["expired"] = "true"; + res["expired"] = TRUE_STR; } if (entry->customData()->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) { @@ -999,7 +1001,7 @@ bool BrowserService::removeFirstDomain(QString& hostname) return false; } -bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, const QString& url) +bool BrowserService::handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl) { if (entryUrl.isEmpty()) { return false; @@ -1016,32 +1018,36 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, } } + // Make a direct compare if a local file is used + if (url.contains("file://")) { + return entryUrl == submitUrl; + } + // URL host validation fails - if (browserSettings()->matchUrlScheme() && entryQUrl.host().isEmpty()) { + if (entryQUrl.host().isEmpty()) { return false; } // Match port, if used - QUrl qUrl(url); - if (entryQUrl.port() > 0 && entryQUrl.port() != qUrl.port()) { + QUrl siteQUrl(url); + if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) { return false; } // Match scheme if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty() - && entryQUrl.scheme().compare(qUrl.scheme()) != 0) { + && entryQUrl.scheme().compare(siteQUrl.scheme()) != 0) { return false; } // Check for illegal characters QRegularExpression re("[<>\\^`{|}]"); - auto match = re.match(entryUrl); - if (match.hasMatch()) { + if (re.match(entryUrl).hasMatch()) { return false; } // Filter to match hostname in URL field - if (entryQUrl.host().endsWith(hostname)) { + if (siteQUrl.host().endsWith(entryQUrl.host())) { return true; } diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index cb20ecbfb7..495c9ac258 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -63,8 +63,8 @@ class BrowserService : public QObject const QString& group, const QString& groupUuid, const QSharedPointer& selectedDb = {}); - QList searchEntries(const QSharedPointer& db, const QString& hostname, const QString& url); - QList searchEntries(const QString& url, const StringPairList& keyList); + QList searchEntries(const QSharedPointer& db, const QString& url, const QString& submitUrl); + QList searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList); void convertAttributesToCustomData(const QSharedPointer& currentDb = {}); public: @@ -74,6 +74,7 @@ class BrowserService : public QObject static const QString LEGACY_ASSOCIATE_KEY_PREFIX; static const QString OPTION_SKIP_AUTO_SUBMIT; static const QString OPTION_HIDE_ENTRY; + static const QString OPTION_ONLY_HTTP_AUTH; static const QString ADDITIONAL_URL; public slots: @@ -130,7 +131,7 @@ public slots: sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const; bool schemeFound(const QString& url); bool removeFirstDomain(QString& hostname); - bool handleURL(const QString& entryUrl, const QString& hostname, const QString& url); + bool handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl); QString baseDomain(const QString& hostname) const; QSharedPointer getDatabase(); QSharedPointer selectedDatabase(); diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index f02794a4bb..5855eff469 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -30,8 +30,7 @@ const QCommandLineOption Merge::SameCredentialsOption = QObject::tr("Use the same credentials for both database files.")); const QCommandLineOption Merge::KeyFileFromOption = - QCommandLineOption(QStringList() << "k" - << "key-file-from", + QCommandLineOption(QStringList() << "key-file-from", QObject::tr("Key file of the database to merge from."), QObject::tr("path")); diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index 98cc6be06a..179b79a435 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -149,8 +149,7 @@ void enterInteractiveMode(const QStringList& arguments) prompt += "> "; command = reader->readLine(prompt); if (reader->isFinished()) { - currentDatabase->releaseData(); - return; + break; } QStringList args = Utils::splitCommandString(command); @@ -163,14 +162,17 @@ void enterInteractiveMode(const QStringList& arguments) errorTextStream << QObject::tr("Unknown command %1").arg(args[0]) << "\n"; continue; } else if (cmd->name == "quit" || cmd->name == "exit") { - currentDatabase->releaseData(); - return; + break; } cmd->currentDatabase = currentDatabase; cmd->execute(args); currentDatabase = cmd->currentDatabase; } + + if (currentDatabase) { + currentDatabase->releaseData(); + } } int main(int argc, char** argv) diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index 2d1a3e0878..f6cd1d3f67 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -106,7 +106,11 @@ namespace Bootstrap { // start minimized if configured if (config()->get("GUI/MinimizeOnStartup").toBool()) { +#ifdef Q_OS_WIN mainWindow.showMinimized(); +#else + mainWindow.hideWindow(); +#endif } else { mainWindow.bringToFront(); } diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 4cccd6d534..066abc4e31 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -42,7 +42,6 @@ Database::Database() : m_metadata(new Metadata(this)) , m_data() , m_rootGroup(nullptr) - , m_timer(new QTimer(this)) , m_fileWatcher(new FileWatcher(this)) , m_emitModified(false) , m_uuid(QUuid::createUuid()) @@ -50,12 +49,12 @@ Database::Database() setRootGroup(new Group()); rootGroup()->setUuid(QUuid::createUuid()); rootGroup()->setName(tr("Root", "Root group name")); - m_timer->setSingleShot(true); + m_modifiedTimer.setSingleShot(true); s_uuidMap.insert(m_uuid, this); connect(m_metadata, SIGNAL(metadataModified()), SLOT(markAsModified())); - connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified())); + connect(&m_modifiedTimer, SIGNAL(timeout()), SIGNAL(databaseModified())); connect(this, SIGNAL(databaseOpened()), SLOT(updateCommonUsernames())); connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames())); connect(m_fileWatcher, SIGNAL(fileChanged()), SIGNAL(databaseFileChanged())); @@ -229,6 +228,7 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath; bool ok = performSave(canonicalFilePath, error, atomic, backup); if (ok) { + markAsClean(); setFilePath(filePath); m_fileWatcher->start(canonicalFilePath, 30, 1); } else { @@ -343,7 +343,6 @@ bool Database::writeDatabase(QIODevice* device, QString* error) return false; } - markAsClean(); return true; } @@ -832,7 +831,7 @@ void Database::emptyRecycleBin() void Database::setEmitModified(bool value) { if (m_emitModified && !value) { - m_timer->stop(); + m_modifiedTimer.stop(); } m_emitModified = value; @@ -846,8 +845,9 @@ bool Database::isModified() const void Database::markAsModified() { m_modified = true; - if (m_emitModified) { - startModifiedTimer(); + if (m_emitModified && !m_modifiedTimer.isActive()) { + // Small time delay prevents numerous consecutive saves due to repeated signals + m_modifiedTimer.start(150); } } @@ -855,6 +855,7 @@ void Database::markAsClean() { bool emitSignal = m_modified; m_modified = false; + m_modifiedTimer.stop(); if (emitSignal) { emit databaseSaved(); } @@ -869,18 +870,6 @@ Database* Database::databaseByUuid(const QUuid& uuid) return s_uuidMap.value(uuid, nullptr); } -void Database::startModifiedTimer() -{ - if (!m_emitModified) { - return; - } - - if (m_timer->isActive()) { - m_timer->stop(); - } - m_timer->start(150); -} - QSharedPointer Database::key() const { return m_data.key; diff --git a/src/core/Database.h b/src/core/Database.h index d5f2092b21..d3d88e7d2e 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -21,9 +21,9 @@ #include #include -#include #include #include +#include #include "config-keepassx.h" #include "crypto/kdf/AesKdf.h" @@ -37,7 +37,6 @@ enum class EntryReferenceType; class FileWatcher; class Group; class Metadata; -class QTimer; class QIODevice; struct DeletedObject @@ -155,9 +154,6 @@ public slots: void databaseDiscarded(); void databaseFileChanged(); -private slots: - void startModifiedTimer(); - private: struct DatabaseData { @@ -211,7 +207,7 @@ private slots: DatabaseData m_data; QPointer m_rootGroup; QList m_deletedObjects; - QPointer m_timer; + QTimer m_modifiedTimer; QPointer m_fileWatcher; bool m_initialized = false; bool m_modified = false; diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp index 0d1def31ae..fb8e951280 100644 --- a/src/core/FileWatcher.cpp +++ b/src/core/FileWatcher.cpp @@ -35,11 +35,10 @@ namespace FileWatcher::FileWatcher(QObject* parent) : QObject(parent) - , m_ignoreFileChange(false) { - connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(onWatchedFileChanged())); + connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(checkFileChanged())); + connect(&m_fileChecksumTimer, SIGNAL(timeout()), SLOT(checkFileChanged())); connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged())); - connect(&m_fileChecksumTimer, SIGNAL(timeout()), SLOT(checkFileChecksum())); m_fileChangeDelayTimer.setSingleShot(true); m_fileIgnoreDelayTimer.setSingleShot(true); } @@ -101,17 +100,6 @@ void FileWatcher::resume() } } -void FileWatcher::onWatchedFileChanged() -{ - // Don't notify if we are ignoring events or already started a notification chain - if (shouldIgnoreChanges()) { - return; - } - - m_fileChecksum = calculateChecksum(); - m_fileChangeDelayTimer.start(0); -} - bool FileWatcher::shouldIgnoreChanges() { return m_filePath.isEmpty() || m_ignoreFileChange || m_fileIgnoreDelayTimer.isActive() @@ -123,15 +111,23 @@ bool FileWatcher::hasSameFileChecksum() return calculateChecksum() == m_fileChecksum; } -void FileWatcher::checkFileChecksum() +void FileWatcher::checkFileChanged() { if (shouldIgnoreChanges()) { return; } - if (!hasSameFileChecksum()) { - onWatchedFileChanged(); + // Prevent reentrance + m_ignoreFileChange = true; + + // Only trigger the change notice if there is a checksum mismatch + auto checksum = calculateChecksum(); + if (checksum != m_fileChecksum) { + m_fileChecksum = checksum; + m_fileChangeDelayTimer.start(0); } + + m_ignoreFileChange = false; } QByteArray FileWatcher::calculateChecksum() diff --git a/src/core/FileWatcher.h b/src/core/FileWatcher.h index fea05fc84b..9b55badc16 100644 --- a/src/core/FileWatcher.h +++ b/src/core/FileWatcher.h @@ -43,8 +43,7 @@ public slots: void resume(); private slots: - void onWatchedFileChanged(); - void checkFileChecksum(); + void checkFileChanged(); private: QByteArray calculateChecksum(); @@ -56,8 +55,8 @@ private slots: QTimer m_fileChangeDelayTimer; QTimer m_fileIgnoreDelayTimer; QTimer m_fileChecksumTimer; - int m_fileChecksumSizeBytes; - bool m_ignoreFileChange; + int m_fileChecksumSizeBytes = -1; + bool m_ignoreFileChange = false; }; class BulkFileWatcher : public QObject diff --git a/src/core/Global.h b/src/core/Global.h index 9ebe787902..0821687e36 100644 --- a/src/core/Global.h +++ b/src/core/Global.h @@ -20,6 +20,7 @@ #ifndef KEEPASSX_GLOBAL_H #define KEEPASSX_GLOBAL_H +#include #include #if defined(Q_OS_WIN) @@ -42,6 +43,9 @@ #define FILE_CASE_SENSITIVE Qt::CaseSensitive #endif +static const auto TRUE_STR = QStringLiteral("true"); +static const auto FALSE_STR = QStringLiteral("false"); + template struct AddConst { typedef const T Type; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 2dbf0093d6..7cfc8a55f3 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -259,6 +260,33 @@ namespace Tools } } + bool checkUrlValid(const QString& urlField) + { + if (urlField.isEmpty()) { + return true; + } + + QUrl url; + if (urlField.contains("://")) { + url = urlField; + } else { + url = QUrl::fromUserInput(urlField); + } + + if (url.scheme() != "file" && url.host().isEmpty()) { + return false; + } + + // Check for illegal characters. Adds also the wildcard * to the list + QRegularExpression re("[<>\\^`{|}\\*]"); + auto match = re.match(urlField); + if (match.hasMatch()) { + return false; + } + + return true; + } + // Escape common regex symbols except for *, ?, and | auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); diff --git a/src/core/Tools.h b/src/core/Tools.h index 1fa5e6a9ad..455b879c25 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -41,6 +41,7 @@ namespace Tools bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); + bool checkUrlValid(const QString& urlField); QString uuidToHex(const QUuid& uuid); QUuid hexToUuid(const QString& uuid); QRegularExpression convertToRegex(const QString& string, diff --git a/src/fdosecrets/CMakeLists.txt b/src/fdosecrets/CMakeLists.txt index 9d3fcb6a92..a9750bc2d2 100644 --- a/src/fdosecrets/CMakeLists.txt +++ b/src/fdosecrets/CMakeLists.txt @@ -4,6 +4,7 @@ if(WITH_XC_FDOSECRETS) add_library(fdosecrets STATIC # app settings page FdoSecretsPlugin.cpp + widgets/SettingsModels.cpp widgets/SettingsWidgetFdoSecrets.cpp # per database settings page diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp index 668b5fb046..646f853018 100644 --- a/src/fdosecrets/FdoSecretsPlugin.cpp +++ b/src/fdosecrets/FdoSecretsPlugin.cpp @@ -60,11 +60,15 @@ void FdoSecretsPlugin::updateServiceState() }); if (!m_secretService->initialize()) { m_secretService.reset(); + FdoSecrets::settings()->setEnabled(false); + return; } + emit secretServiceStarted(); } } else { if (m_secretService) { m_secretService.reset(); + emit secretServiceStopped(); } } } @@ -74,6 +78,11 @@ Service* FdoSecretsPlugin::serviceInstance() const return m_secretService.data(); } +DatabaseTabWidget* FdoSecretsPlugin::dbTabs() const +{ + return m_dbTabs; +} + void FdoSecretsPlugin::emitRequestSwitchToDatabases() { emit requestSwitchToDatabases(); diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h index 2a57ea0db3..828c0bd764 100644 --- a/src/fdosecrets/FdoSecretsPlugin.h +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -59,6 +59,11 @@ class FdoSecretsPlugin : public QObject, public ISettingsPage */ FdoSecrets::Service* serviceInstance() const; + /** + * @return The db tabs widget, containing opened databases. Can be nullptr. + */ + DatabaseTabWidget* dbTabs() const; + public slots: void emitRequestSwitchToDatabases(); void emitRequestShowNotification(const QString& msg, const QString& title = {}); @@ -67,6 +72,8 @@ public slots: void error(const QString& msg); void requestSwitchToDatabases(); void requestShowNotification(const QString& msg, const QString& title, int msTimeoutHint); + void secretServiceStarted(); + void secretServiceStopped(); private: QPointer m_dbTabs; diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp index c826c1db06..ef277e89b8 100644 --- a/src/fdosecrets/objects/Collection.cpp +++ b/src/fdosecrets/objects/Collection.cpp @@ -21,6 +21,7 @@ #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" #include "core/Config.h" #include "core/Database.h" @@ -284,8 +285,13 @@ namespace FdoSecrets return ret; } + if (!pathToObject(secret.session)) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + } + prompt = nullptr; + bool newlyCreated = true; Item* item = nullptr; QString itemPath; StringStringMap attributes; @@ -303,6 +309,7 @@ namespace FdoSecrets } if (!existings.value().isEmpty() && replace) { item = existings.value().front(); + newlyCreated = false; } } @@ -328,15 +335,25 @@ namespace FdoSecrets // when creation finishes in backend, we will already have item item = m_entryToItem.value(entry, nullptr); - Q_ASSERT(item); + + if (!item) { + // may happen if entry somehow ends up in recycle bin + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + } } ret = item->setProperties(properties); if (ret.isError()) { + if (newlyCreated) { + item->doDelete(); + } return ret; } ret = item->setSecret(secret); if (ret.isError()) { + if (newlyCreated) { + item->doDelete(); + } return ret; } @@ -439,7 +456,7 @@ namespace FdoSecrets auto newUuid = FdoSecrets::settings()->exposedGroup(m_backend->database()); auto newGroup = m_backend->database()->rootGroup()->findGroupByUuid(newUuid); - if (!newGroup) { + if (!newGroup || inRecycleBin(newGroup)) { // no exposed group, delete self doDelete(); return; @@ -451,14 +468,20 @@ namespace FdoSecrets m_exposedGroup = newGroup; // Attach signal to update exposed group settings if the group was removed. - // The lifetime of the connection is bound to the database object, because - // in Database::~Database, groups are also deleted, but we don't want to - // trigger this. - // This rely on the fact that QObject disconnects signals BEFORE deleting - // children. + // + // When the group object is normally deleted due to ~Database, the databaseReplaced + // signal should be first emitted, and we will clean up connection in reloadDatabase, + // so this handler won't be triggered. QPointer db = m_backend->database().data(); - connect(m_exposedGroup.data(), &Group::groupAboutToRemove, db, [db](Group* toBeRemoved) { - if (!db) { + connect(m_exposedGroup.data(), &Group::groupAboutToRemove, this, [this](Group* toBeRemoved) { + if (backendLocked()) { + return; + } + auto db = m_backend->database(); + if (toBeRemoved->database() != db) { + // should not happen, but anyway. + // somehow our current database has been changed, and the old group is being deleted + // possibly logic changes in replaceDatabase. return; } auto uuid = FdoSecrets::settings()->exposedGroup(db); @@ -468,10 +491,17 @@ namespace FdoSecrets FdoSecrets::settings()->setExposedGroup(db, {}); } }); + // Another possibility is the group being moved to recycle bin. + connect(m_exposedGroup.data(), &Group::groupModified, this, [this]() { + if (inRecycleBin(m_exposedGroup->parentGroup())) { + // reset the exposed group to none + FdoSecrets::settings()->setExposedGroup(m_backend->database().data(), {}); + } + }); // Monitor exposed group settings connect(m_backend->database()->metadata()->customData(), &CustomData::customDataModified, this, [this]() { - if (!m_exposedGroup || !m_backend) { + if (!m_exposedGroup || backendLocked()) { return; } if (m_exposedGroup->uuid() == FdoSecrets::settings()->exposedGroup(m_backend->database())) { @@ -590,9 +620,13 @@ namespace FdoSecrets void Collection::cleanupConnections() { + m_backend->database()->metadata()->customData()->disconnect(this); if (m_exposedGroup) { - m_exposedGroup->disconnect(this); + for (const auto group : m_exposedGroup->groupsRecursive(true)) { + group->disconnect(this); + } } + m_items.clear(); } @@ -646,17 +680,21 @@ namespace FdoSecrets { Q_ASSERT(m_backend); - if (!m_backend->database()->metadata()->recycleBin()) { + if (!group) { + // the root group's parent is nullptr, we treat it as not in recycle bin. return false; } - while (group) { - if (group->uuid() == m_backend->database()->metadata()->recycleBin()->uuid()) { - return true; - } - group = group->parentGroup(); + if (!m_backend->database()->metadata()) { + return false; } - return false; + + auto recycleBin = m_backend->database()->metadata()->recycleBin(); + if (!recycleBin) { + return false; + } + + return group->uuid() == recycleBin->uuid() || group->isRecycled(); } bool Collection::inRecycleBin(Entry* entry) const diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h index de9db3a494..06d45a50ee 100644 --- a/src/fdosecrets/objects/Collection.h +++ b/src/fdosecrets/objects/Collection.h @@ -74,6 +74,11 @@ namespace FdoSecrets public: DBusReturn setProperties(const QVariantMap& properties); + bool isValid() const + { + return backend(); + } + DBusReturn removeAlias(QString alias); DBusReturn addAlias(QString alias); const QSet aliases() const; @@ -106,6 +111,7 @@ namespace FdoSecrets private slots: void onDatabaseLockChanged(); void onDatabaseExposedGroupChanged(); + // force reload info from backend, potentially delete self void reloadBackend(); private: diff --git a/src/fdosecrets/objects/DBusReturn.h b/src/fdosecrets/objects/DBusReturn.h index 6c94eab183..889b8e11c7 100644 --- a/src/fdosecrets/objects/DBusReturn.h +++ b/src/fdosecrets/objects/DBusReturn.h @@ -158,6 +158,12 @@ namespace FdoSecrets return std::move(m_value); } + /** + * Get value or handle the error by the passed in dbus object + * @tparam P + * @param p + * @return + */ template T valueOrHandle(P* p) const& { if (isError()) { @@ -169,6 +175,12 @@ namespace FdoSecrets return m_value; } + /** + * Get value or handle the error by the passed in dbus object + * @tparam P + * @param p + * @return + */ template T&& valueOrHandle(P* p) && { if (isError()) { diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index 6bca1f12c6..a2d478cae2 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -47,7 +47,6 @@ namespace FdoSecrets , m_insdieEnsureDefaultAlias(false) , m_serviceWatcher(nullptr) { - registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this)); } Service::~Service() @@ -64,6 +63,8 @@ namespace FdoSecrets return false; } + registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this)); + // Connect to service unregistered signal m_serviceWatcher.reset(new QDBusServiceWatcher()); connect(m_serviceWatcher.data(), @@ -93,7 +94,24 @@ namespace FdoSecrets void Service::onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal) { + // The Collection will monitor the database's exposed group. + // When the Collection finds that no exposed group, it will delete itself. + // Thus the service also needs to monitor it and recreate the collection if the user changes + // from no exposed to exposed something. + if (!dbWidget->isLocked()) { + monitorDatabaseExposedGroup(dbWidget); + } + connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() { + monitorDatabaseExposedGroup(dbWidget); + }); + auto coll = new Collection(this, dbWidget); + // Creation may fail if the database is not exposed. + // This is okay, because we monitor the expose settings above + if (!coll->isValid()) { + coll->deleteLater(); + return; + } m_collections << coll; m_dbToCollection[dbWidget] = coll; @@ -127,15 +145,6 @@ namespace FdoSecrets emit collectionDeleted(coll); }); - // a special case: the database changed from no expose to expose something. - // in this case, there is no collection out there monitoring it, so create a new collection - if (!dbWidget->isLocked()) { - monitorDatabaseExposedGroup(dbWidget); - } - connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() { - monitorDatabaseExposedGroup(dbWidget); - }); - if (emitSignal) { emit collectionCreated(coll); } diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp index fadd015422..16a780de7e 100644 --- a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp @@ -85,7 +85,7 @@ class DatabaseSettingsWidgetFdoSecrets::GroupModelNoRecycle : public QSortFilter // can not call mapFromSource, which internally calls filterAcceptsRow auto group = groupFromSourceIndex(source_idx); - return group->uuid() != recycleBin->uuid(); + return group && !group->isRecycled() && group->uuid() != recycleBin->uuid(); } }; @@ -118,8 +118,13 @@ void DatabaseSettingsWidgetFdoSecrets::loadSettings(QSharedPointer db) m_model.reset(new GroupModelNoRecycle(m_db.data())); m_ui->selectGroup->setModel(m_model.data()); + Group* recycleBin = nullptr; + if (m_db->metadata() && m_db->metadata()->recycleBin()) { + recycleBin = m_db->metadata()->recycleBin(); + } + auto group = m_db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(m_db)); - if (!group) { + if (!group || group->isRecycled() || (recycleBin && group->uuid() == recycleBin->uuid())) { m_ui->radioDonotExpose->setChecked(true); } else { auto idx = m_model->indexFromGroup(group); diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp new file mode 100644 index 0000000000..edcb275c8b --- /dev/null +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -0,0 +1,396 @@ +/* + * Copyright (C) 2019 Aetf + * + * 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 "SettingsModels.h" + +#include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" + +#include "core/Database.h" +#include "core/DatabaseIcons.h" +#include "core/FilePath.h" +#include "gui/DatabaseTabWidget.h" +#include "gui/DatabaseWidget.h" + +#include + +namespace FdoSecrets +{ + + SettingsDatabaseModel::SettingsDatabaseModel(DatabaseTabWidget* dbTabs, QObject* parent) + : QAbstractTableModel(parent) + , m_dbTabs(nullptr) + { + setTabWidget(dbTabs); + } + + void SettingsDatabaseModel::setTabWidget(DatabaseTabWidget* dbTabs) + { + auto old = m_dbTabs; + m_dbTabs = dbTabs; + if (old != m_dbTabs) { + populateModel(); + } + } + + int SettingsDatabaseModel::rowCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return m_dbs.size(); + } + + int SettingsDatabaseModel::columnCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return 3; + } + + QVariant SettingsDatabaseModel::headerData(int section, Qt::Orientation orientation, int role) const + { + if (orientation != Qt::Horizontal) { + return {}; + } + + if (role != Qt::DisplayRole) { + return {}; + } + + switch (section) { + case 0: + return tr("File Name"); + case 1: + return tr("Group"); + case 2: + return tr("Manage"); + default: + return {}; + } + } + + QVariant SettingsDatabaseModel::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) { + return {}; + } + const auto& dbWidget = m_dbs[index.row()]; + if (!dbWidget) { + return {}; + } + + switch (index.column()) { + case 0: + return dataForName(dbWidget, role); + case 1: + return dataForExposedGroup(dbWidget, role); + case 2: + return dataForManage(dbWidget, role); + default: + return {}; + } + } + + QVariant SettingsDatabaseModel::dataForName(DatabaseWidget* db, int role) const + { + switch (role) { + case Qt::DisplayRole: { + QFileInfo fi(db->database()->filePath()); + return fi.fileName(); + } + case Qt::ToolTipRole: + return db->database()->filePath(); + default: + return {}; + } + } + + QVariant SettingsDatabaseModel::dataForExposedGroup(DatabaseWidget* dbWidget, int role) + { + if (dbWidget->isLocked()) { + switch (role) { + case Qt::DisplayRole: + return tr("Unlock to show"); + case Qt::DecorationRole: + return filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked"), true); + case Qt::FontRole: { + QFont font; + font.setItalic(true); + return font; + } + default: + return {}; + } + } + auto db = dbWidget->database(); + auto group = db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(db)); + if (group) { + switch (role) { + case Qt::DisplayRole: + return group->name(); + case Qt::DecorationRole: + return group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex) + : group->iconScaledPixmap(); + case Qt::FontRole: + if (group->isExpired()) { + QFont font; + font.setStrikeOut(true); + return font; + } else { + return {}; + } + default: + return {}; + } + } else { + switch (role) { + case Qt::DisplayRole: + return tr("None"); + case Qt::DecorationRole: + return filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none"), true); + default: + return {}; + } + } + } + + QVariant SettingsDatabaseModel::dataForManage(DatabaseWidget* db, int role) const + { + switch (role) { + case Qt::EditRole: + return QVariant::fromValue(db); + default: + return {}; + } + } + + void SettingsDatabaseModel::populateModel() + { + beginResetModel(); + + m_dbs.clear(); + + if (m_dbTabs) { + // Add existing database tabs + for (int idx = 0; idx != m_dbTabs->count(); ++idx) { + auto dbWidget = m_dbTabs->databaseWidgetFromIndex(idx); + databaseAdded(dbWidget, false); + } + // connect signals + connect(m_dbTabs, &DatabaseTabWidget::databaseOpened, this, [this](DatabaseWidget* db) { + databaseAdded(db, true); + }); + connect(m_dbTabs, &DatabaseTabWidget::databaseClosed, this, &SettingsDatabaseModel::databaseRemoved); + } + + endResetModel(); + } + + void SettingsDatabaseModel::databaseAdded(DatabaseWidget* db, bool emitSignals) + { + int row = m_dbs.size(); + if (emitSignals) { + beginInsertRows({}, row, row); + } + + m_dbs.append(db); + connect(db, &DatabaseWidget::databaseLocked, this, [row, this]() { + emit dataChanged(index(row, 1), index(row, 2)); + }); + connect(db, &DatabaseWidget::databaseUnlocked, this, [row, this]() { + emit dataChanged(index(row, 1), index(row, 2)); + }); + connect(db, &DatabaseWidget::databaseModified, this, [row, this]() { + emit dataChanged(index(row, 0), index(row, 2)); + }); + connect(db, &DatabaseWidget::databaseFilePathChanged, this, [row, this]() { + emit dataChanged(index(row, 0), index(row, 2)); + }); + + if (emitSignals) { + endInsertRows(); + } + } + + void SettingsDatabaseModel::databaseRemoved(const QString& filePath) + { + for (int i = 0; i != m_dbs.size(); i++) { + if (m_dbs[i] && m_dbs[i]->database()->filePath() == filePath) { + beginRemoveRows({}, i, i); + + m_dbs[i]->disconnect(this); + m_dbs.removeAt(i); + + endRemoveRows(); + break; + } + } + } + + SettingsSessionModel::SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent) + : QAbstractTableModel(parent) + , m_service(nullptr) + { + setService(plugin->serviceInstance()); + connect(plugin, &FdoSecretsPlugin::secretServiceStarted, this, [plugin, this]() { + setService(plugin->serviceInstance()); + }); + connect(plugin, &FdoSecretsPlugin::secretServiceStopped, this, [this]() { setService(nullptr); }); + } + + void SettingsSessionModel::setService(Service* service) + { + auto old = m_service; + m_service = service; + if (old != m_service) { + populateModel(); + } + } + + int SettingsSessionModel::rowCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return m_sessions.size(); + } + + int SettingsSessionModel::columnCount(const QModelIndex& parent) const + { + if (parent.isValid()) { + return 0; + } + return 2; + } + + QVariant SettingsSessionModel::headerData(int section, Qt::Orientation orientation, int role) const + { + if (orientation != Qt::Horizontal) { + return {}; + } + + if (role != Qt::DisplayRole) { + return {}; + } + + switch (section) { + case 0: + return tr("Application"); + case 1: + return tr("Manage"); + default: + return {}; + } + } + + QVariant SettingsSessionModel::data(const QModelIndex& index, int role) const + { + if (!index.isValid()) { + return {}; + } + const auto& sess = m_sessions[index.row()]; + if (!sess) { + return {}; + } + + switch (index.column()) { + case 0: + return dataForApplication(sess, role); + case 1: + return dataForManage(sess, role); + default: + return {}; + } + } + + QVariant SettingsSessionModel::dataForApplication(Session* sess, int role) const + { + switch (role) { + case Qt::DisplayRole: + return sess->peer(); + default: + return {}; + } + } + + QVariant SettingsSessionModel::dataForManage(Session* sess, int role) const + { + switch (role) { + case Qt::EditRole: { + auto v = QVariant::fromValue(sess); + qDebug() << v << v.type() << v.userType(); + return v; + } + default: + return {}; + } + } + + void SettingsSessionModel::populateModel() + { + beginResetModel(); + + m_sessions.clear(); + + if (m_service) { + // Add existing database tabs + for (const auto& sess : m_service->sessions()) { + sessionAdded(sess, false); + } + + // connect signals + connect(m_service, &Service::sessionOpened, this, [this](Session* sess) { sessionAdded(sess, true); }); + connect(m_service, &Service::sessionClosed, this, &SettingsSessionModel::sessionRemoved); + } + + endResetModel(); + } + + void SettingsSessionModel::sessionAdded(Session* sess, bool emitSignals) + { + int row = m_sessions.size(); + if (emitSignals) { + beginInsertRows({}, row, row); + } + + m_sessions.append(sess); + + if (emitSignals) { + endInsertRows(); + } + } + + void SettingsSessionModel::sessionRemoved(Session* sess) + { + for (int i = 0; i != m_sessions.size(); i++) { + if (m_sessions[i] == sess) { + beginRemoveRows({}, i, i); + + m_sessions[i]->disconnect(this); + m_sessions.removeAt(i); + + endRemoveRows(); + break; + } + } + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/widgets/SettingsModels.h b/src/fdosecrets/widgets/SettingsModels.h new file mode 100644 index 0000000000..b07bb16374 --- /dev/null +++ b/src/fdosecrets/widgets/SettingsModels.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2019 Aetf + * + * 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_FDOSECRETS_SETTINGSMODELS_H +#define KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H + +#include +#include + +class DatabaseTabWidget; +class DatabaseWidget; +class FdoSecretsPlugin; + +namespace FdoSecrets +{ + class SettingsDatabaseModel : public QAbstractTableModel + { + Q_OBJECT + public: + explicit SettingsDatabaseModel(DatabaseTabWidget* dbTabs, QObject* parent = nullptr); + + void setTabWidget(DatabaseTabWidget* dbTabs); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + private: + QVariant dataForName(DatabaseWidget* db, int role) const; + static QVariant dataForExposedGroup(DatabaseWidget* db, int role); + QVariant dataForManage(DatabaseWidget* db, int role) const; + + private slots: + void populateModel(); + void databaseAdded(DatabaseWidget* db, bool emitSignals); + void databaseRemoved(const QString& filePath); + + private: + // source + QPointer m_dbTabs; + + // internal store + QList> m_dbs; + }; + + class Service; + class Session; + + class SettingsSessionModel : public QAbstractTableModel + { + Q_OBJECT + public: + explicit SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + private: + void setService(Service* service); + + QVariant dataForApplication(Session* sess, int role) const; + QVariant dataForManage(Session* sess, int role) const; + + private slots: + void populateModel(); + void sessionAdded(Session* sess, bool emitSignals); + void sessionRemoved(Session* sess); + + private: + // source + QPointer m_service; + + // internal copy, so we can emit with changed index + QList m_sessions; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 920b603d99..59399cdec0 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -20,241 +20,273 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" -#include "fdosecrets/objects/Collection.h" -#include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Session.h" +#include "fdosecrets/widgets/SettingsModels.h" -#include "core/DatabaseIcons.h" #include "core/FilePath.h" #include "gui/DatabaseWidget.h" #include -#include -#include #include -#include -#include +#include +#include #include #include -using FdoSecrets::Collection; -using FdoSecrets::Service; using FdoSecrets::Session; +using FdoSecrets::SettingsDatabaseModel; +using FdoSecrets::SettingsSessionModel; -SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::SettingsWidgetFdoSecrets()) - , m_plugin(plugin) +namespace { - m_ui->setupUi(this); + class ManageDatabase : public QToolBar + { + Q_OBJECT + + Q_PROPERTY(DatabaseWidget* dbWidget READ dbWidget WRITE setDbWidget USER true) + + public: + explicit ManageDatabase(FdoSecretsPlugin* plugin, QWidget* parent = nullptr) + : QToolBar(parent) + , m_plugin(plugin) + { + setFloatable(false); + setMovable(false); + + // use a dummy widget to center the buttons + auto spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->setVisible(true); + addWidget(spacer); + + // db settings + m_dbSettingsAct = new QAction(tr("Database settings"), this); + m_dbSettingsAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("document-edit"))); + m_dbSettingsAct->setToolTip(tr("Edit database settings")); + m_dbSettingsAct->setEnabled(false); + connect(m_dbSettingsAct, &QAction::triggered, this, [this]() { + if (!m_dbWidget) { + return; + } + auto db = m_dbWidget; + m_plugin->serviceInstance()->doSwitchToChangeDatabaseSettings(m_dbWidget); + }); + addAction(m_dbSettingsAct); + + // unlock/lock + m_lockAct = new QAction(tr("Unlock database"), this); + m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), false)); + m_lockAct->setToolTip(tr("Unlock database to show more information")); + connect(m_lockAct, &QAction::triggered, this, [this]() { + if (!m_dbWidget) { + return; + } + if (m_dbWidget->isLocked()) { + m_plugin->serviceInstance()->doUnlockDatabaseInDialog(m_dbWidget); + } else { + m_dbWidget->lock(); + } + }); + + addAction(m_lockAct); + + // use a dummy widget to center the buttons + spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->setVisible(true); + addWidget(spacer); + } - auto sessHeader = m_ui->tableSessions->horizontalHeader(); - sessHeader->setSelectionMode(QAbstractItemView::NoSelection); - sessHeader->setSectionsClickable(false); - sessHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application - sessHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button + DatabaseWidget* dbWidget() const + { + return m_dbWidget; + } - auto dbHeader = m_ui->tableDatabases->horizontalHeader(); - dbHeader->setSelectionMode(QAbstractItemView::NoSelection); - dbHeader->setSectionsClickable(false); - dbHeader->setSectionResizeMode(0, QHeaderView::Stretch); // file name - dbHeader->setSectionResizeMode(1, QHeaderView::Stretch); // group - dbHeader->setSectionResizeMode(2, QHeaderView::ResizeToContents); // manage button + void setDbWidget(DatabaseWidget* dbWidget) + { + if (m_dbWidget == dbWidget) { + return; + } - m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked()); - connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled); -} + if (m_dbWidget) { + disconnect(); + } -SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default; + m_dbWidget = dbWidget; -void SettingsWidgetFdoSecrets::populateSessions(bool enabled) -{ - m_ui->tableSessions->setRowCount(0); + reconnect(); + } - auto service = m_plugin->serviceInstance(); - if (!service || !enabled) { - return; - } + private: + void disconnect() + { + if (!m_dbWidget) { + return; + } + m_dbWidget->disconnect(this); + } - for (const auto& sess : service->sessions()) { - addSessionRow(sess); - } -} + void reconnect() + { + if (!m_dbWidget) { + return; + } + connect(m_dbWidget, &DatabaseWidget::databaseLocked, this, [this]() { + m_lockAct->setText(tr("Unlock database")); + m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), false)); + m_lockAct->setToolTip(tr("Unlock database to show more information")); + m_dbSettingsAct->setEnabled(false); + }); + connect(m_dbWidget, &DatabaseWidget::databaseUnlocked, this, [this]() { + m_lockAct->setText(tr("Lock database")); + m_lockAct->setIcon( + filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"), false)); + m_lockAct->setToolTip(tr("Lock database")); + m_dbSettingsAct->setEnabled(true); + }); + } -void SettingsWidgetFdoSecrets::addSessionRow(Session* sess) -{ - auto row = m_ui->tableSessions->rowCount(); - m_ui->tableSessions->insertRow(row); + private: + FdoSecretsPlugin* m_plugin = nullptr; + QPointer m_dbWidget = nullptr; + QAction* m_dbSettingsAct = nullptr; + QAction* m_lockAct = nullptr; + }; + + class ManageSession : public QToolBar + { + Q_OBJECT + + Q_PROPERTY(Session* session READ session WRITE setSession USER true) + + public: + explicit ManageSession(FdoSecretsPlugin*, QWidget* parent = nullptr) + : QToolBar(parent) + { + setFloatable(false); + setMovable(false); + + // use a dummy widget to center the buttons + auto spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->setVisible(true); + addWidget(spacer); + + m_disconnectAct = new QAction(tr("Disconnect"), this); + m_disconnectAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("dialog-close"))); + m_disconnectAct->setToolTip(tr("Disconnect this application")); + connect(m_disconnectAct, &QAction::triggered, this, [this]() { + if (m_session) { + m_session->close(); + } + }); + addAction(m_disconnectAct); + + // use a dummy widget to center the buttons + spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->setVisible(true); + addWidget(spacer); + } - // column 0: application name - auto item = new QTableWidgetItem(sess->peer()); - item->setData(Qt::UserRole, QVariant::fromValue(sess)); - m_ui->tableSessions->setItem(row, 0, item); + Session* session() + { + return m_session; + } - // column 1: disconnect button - auto btn = new QPushButton(tr("Disconnect")); - connect(btn, &QPushButton::clicked, sess, &Session::close); - m_ui->tableSessions->setCellWidget(row, 1, btn); + void setSession(Session* sess) + { + m_session = sess; + } - // column 2: hidden uuid - m_ui->tableSessions->setItem(row, 2, new QTableWidgetItem(sess->id())); -} + private: + Session* m_session = nullptr; + QAction* m_disconnectAct = nullptr; + }; + + template class Creator : public QItemEditorCreatorBase + { + public: + inline explicit Creator(FdoSecretsPlugin* plugin) + : QItemEditorCreatorBase() + , m_plugin(plugin) + , m_propertyName(T::staticMetaObject.userProperty().name()) + { + } -void SettingsWidgetFdoSecrets::removeSessionRow(Session* sess) -{ - int row = 0; - while (row != m_ui->tableSessions->rowCount()) { - auto item = m_ui->tableSessions->item(row, 0); - const auto itemSess = item->data(Qt::UserRole).value(); - if (itemSess == sess) { - break; + inline QWidget* createWidget(QWidget* parent) const override + { + return new T(m_plugin, parent); } - ++row; - } - if (row == m_ui->tableSessions->rowCount()) { - qWarning() << "Unknown Fdo Secret Service session" << sess->id() << "while removing collection from table"; - return; - } - - m_ui->tableSessions->removeRow(row); -} -void SettingsWidgetFdoSecrets::populateDatabases(bool enabled) -{ - m_ui->tableDatabases->setRowCount(0); - - auto service = m_plugin->serviceInstance(); - if (!service || !enabled) { - return; - } - - auto ret = service->collections(); - if (ret.isError()) { - return; - } - for (const auto& coll : ret.value()) { - addDatabaseRow(coll); - } -} + inline QByteArray valuePropertyName() const override + { + return m_propertyName; + } -void SettingsWidgetFdoSecrets::addDatabaseRow(Collection* coll) -{ - auto row = m_ui->tableDatabases->rowCount(); - m_ui->tableDatabases->insertRow(row); - - // column 0: File name - QFileInfo fi(coll->backend()->database()->filePath()); - auto item = new QTableWidgetItem(fi.fileName()); - item->setData(Qt::UserRole, QVariant::fromValue(coll)); - m_ui->tableDatabases->setItem(row, 0, item); - - // column 2: manage button: hboxlayout: unlock/lock settings - // create this first so we have a widget to bind connection to, - // which can then be auto deleted when the row is deleted. - auto widget = createManageButtons(coll); - m_ui->tableDatabases->setCellWidget(row, 2, widget); - - // column 1: Group name - auto itemGroupName = new QTableWidgetItem(); - updateExposedGroupItem(itemGroupName, coll); - - connect(coll, &Collection::collectionLockChanged, widget, [this, itemGroupName, coll](bool) { - updateExposedGroupItem(itemGroupName, coll); - }); - - m_ui->tableDatabases->setItem(row, 1, itemGroupName); -} + private: + FdoSecretsPlugin* m_plugin; + QByteArray m_propertyName; + }; +} // namespace -QWidget* SettingsWidgetFdoSecrets::createManageButtons(Collection* coll) +SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::SettingsWidgetFdoSecrets()) + , m_factory(new QItemEditorFactory) + , m_plugin(plugin) { - auto toolbar = new QToolBar; - toolbar->setFloatable(false); - toolbar->setMovable(false); - - // db settings - auto dbSettingsAct = new QAction(tr("Database settings"), toolbar); - dbSettingsAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("document-edit"))); - dbSettingsAct->setToolTip(tr("Edit database settings")); - dbSettingsAct->setEnabled(!coll->locked().value()); - connect(dbSettingsAct, &QAction::triggered, this, [this, coll]() { - auto db = coll->backend(); - m_plugin->serviceInstance()->doSwitchToChangeDatabaseSettings(db); - }); - toolbar->addAction(dbSettingsAct); - - // unlock/lock - auto lockAct = new QAction(tr("Unlock database"), toolbar); - lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true)); - lockAct->setToolTip(tr("Unlock database to show more information")); - connect(coll, &Collection::collectionLockChanged, lockAct, [lockAct, dbSettingsAct](bool locked) { - if (locked) { - lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true)); - lockAct->setToolTip(tr("Unlock database to show more information")); - } else { - lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"), true)); - lockAct->setToolTip(tr("Lock database")); - } - dbSettingsAct->setEnabled(!locked); - }); - connect(lockAct, &QAction::triggered, this, [coll]() { - if (coll->locked().value()) { - coll->doUnlock(); - } else { - coll->doLock(); - } - }); - toolbar->addAction(lockAct); + m_ui->setupUi(this); - return toolbar; -} + auto sessModel = new SettingsSessionModel(plugin, this); + m_ui->tableSessions->setModel(sessModel); + setupView(m_ui->tableSessions, 1, qMetaTypeId(), new Creator(m_plugin)); + + // config header after setting model, otherwise the header doesn't have enough sections + auto sessViewHeader = m_ui->tableSessions->horizontalHeader(); + sessViewHeader->setSelectionMode(QAbstractItemView::NoSelection); + sessViewHeader->setSectionsClickable(false); + sessViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application + sessViewHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button + + auto dbModel = new SettingsDatabaseModel(plugin->dbTabs(), this); + m_ui->tableDatabases->setModel(dbModel); + setupView(m_ui->tableDatabases, 2, qMetaTypeId(), new Creator(m_plugin)); + + // config header after setting model, otherwise the header doesn't have enough sections + auto dbViewHeader = m_ui->tableDatabases->horizontalHeader(); + dbViewHeader->setSelectionMode(QAbstractItemView::NoSelection); + dbViewHeader->setSectionsClickable(false); + dbViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // file name + dbViewHeader->setSectionResizeMode(1, QHeaderView::Stretch); // group + dbViewHeader->setSectionResizeMode(2, QHeaderView::ResizeToContents); // manage button -void SettingsWidgetFdoSecrets::updateExposedGroupItem(QTableWidgetItem* item, Collection* coll) -{ - if (coll->locked().value()) { - item->setText(tr("Unlock to show")); - item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked"), true)); - QFont font; - font.setItalic(true); - item->setFont(font); - return; - } - - auto db = coll->backend()->database(); - auto group = db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(db)); - if (group) { - item->setText(group->name()); - item->setIcon(group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex) - : group->iconScaledPixmap()); - if (group->isExpired()) { - QFont font; - font.setStrikeOut(true); - item->setFont(font); - } - } else { - item->setText(tr("None")); - item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none"), true)); - } + m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked()); + connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled); } -void SettingsWidgetFdoSecrets::removeDatabaseRow(Collection* coll) +void SettingsWidgetFdoSecrets::setupView(QAbstractItemView* view, + int manageColumn, + int editorTypeId, + QItemEditorCreatorBase* creator) { - int row = 0; - while (row != m_ui->tableDatabases->rowCount()) { - auto item = m_ui->tableDatabases->item(row, 0); - const auto itemColl = item->data(Qt::UserRole).value(); - if (itemColl == coll) { - break; - } - ++row; - } - if (row == m_ui->tableDatabases->rowCount()) { - qWarning() << "Unknown Fdo Secret Service collection" << coll->name() << "while removing collection from table"; - return; - } - - m_ui->tableDatabases->removeRow(row); + auto manageButtonDelegate = new QStyledItemDelegate(this); + m_factory->registerEditor(editorTypeId, creator); + manageButtonDelegate->setItemEditorFactory(m_factory.data()); + view->setItemDelegateForColumn(manageColumn, manageButtonDelegate); + connect(view->model(), + &QAbstractItemModel::rowsInserted, + this, + [view, manageColumn](const QModelIndex&, int first, int last) { + for (int i = first; i <= last; ++i) { + auto idx = view->model()->index(i, manageColumn); + view->openPersistentEditor(idx); + } + }); } +SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default; + void SettingsWidgetFdoSecrets::loadSettings() { m_ui->enableFdoSecretService->setChecked(FdoSecrets::settings()->isEnabled()); @@ -269,52 +301,4 @@ void SettingsWidgetFdoSecrets::saveSettings() FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); } -void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) -{ - QWidget::showEvent(event); - - QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, true)); -} - -void SettingsWidgetFdoSecrets::hideEvent(QHideEvent* event) -{ - QWidget::hideEvent(event); - - QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, false)); -} - -void SettingsWidgetFdoSecrets::updateTables(bool enabled) -{ - if (enabled) { - // update the table - populateDatabases(m_ui->enableFdoSecretService->isChecked()); - populateSessions(m_ui->enableFdoSecretService->isChecked()); - - // re-layout the widget to adjust the table cell size - adjustSize(); - - connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions); - connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases); - - auto service = m_plugin->serviceInstance(); - if (service) { - connect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow); - connect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow); - connect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow); - connect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow); - } - } else { - disconnect( - m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions); - disconnect( - m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases); - - auto service = m_plugin->serviceInstance(); - if (service) { - disconnect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow); - disconnect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow); - disconnect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow); - disconnect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow); - } - } -} +#include "SettingsWidgetFdoSecrets.moc" diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index eac1f1e3c8..2bf58f826e 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -21,7 +21,9 @@ #include #include -class QTableWidgetItem; +class QAbstractItemView; +class QItemEditorCreatorBase; +class QItemEditorFactory; namespace FdoSecrets { @@ -48,28 +50,12 @@ public slots: void loadSettings(); void saveSettings(); -private slots: - void populateSessions(bool enabled); - void populateDatabases(bool enabled); - void addSessionRow(FdoSecrets::Session* sess); - void removeSessionRow(FdoSecrets::Session* sess); - void addDatabaseRow(FdoSecrets::Collection* coll); - void removeDatabaseRow(FdoSecrets::Collection* coll); - - void updateTables(bool enabled); - -protected: - void showEvent(QShowEvent* event) override; - - void hideEvent(QHideEvent* event) override; - private: - QWidget* createManageButtons(FdoSecrets::Collection* coll); - - void updateExposedGroupItem(QTableWidgetItem* item, FdoSecrets::Collection* coll); + void setupView(QAbstractItemView* view, int manageColumn, int editorTypeId, QItemEditorCreatorBase* creator); private: QScopedPointer m_ui; + QScopedPointer m_factory; FdoSecretsPlugin* m_plugin; }; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index b77e086c9a..660181f5d1 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -75,7 +75,7 @@ - + Qt::NoFocus @@ -91,21 +91,6 @@ false - - - File Name - - - - - Group - - - - - Manage - - @@ -123,7 +108,7 @@ - + Qt::NoFocus @@ -139,16 +124,6 @@ false - - - Application - - - - - Manage - - diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index f90fd37d1f..c610a773d9 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -204,9 +204,14 @@ void DatabaseOpenWidget::openDatabase() m_db.reset(new Database()); QString error; + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + m_ui->passwordFormFrame->setEnabled(false); + QCoreApplication::processEvents(); bool ok = m_db->open(m_filename, masterKey, &error, false); QApplication::restoreOverrideCursor(); + m_ui->passwordFormFrame->setEnabled(true); + if (!ok) { if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) { QScopedPointer msgBox(new QMessageBox(this)); diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 14a1337c6e..60b2feadc2 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -132,7 +132,7 @@ 15 - + 400 diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 64c61120da..db8e57c230 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -280,6 +280,11 @@ bool DatabaseWidget::isEntryEditActive() const return currentWidget() == m_editEntryWidget; } +bool DatabaseWidget::isGroupEditActive() const +{ + return currentWidget() == m_editGroupWidget; +} + bool DatabaseWidget::isEditWidgetModified() const { if (currentWidget() == m_editEntryWidget) { @@ -392,6 +397,8 @@ void DatabaseWidget::createEntry() void DatabaseWidget::replaceDatabase(QSharedPointer db) { + Q_ASSERT(!isEntryEditActive() && !isGroupEditActive()); + // Save off new parent UUID which will be valid when creating a new entry QUuid newParentUuid; if (m_newParent) { @@ -1381,7 +1388,7 @@ bool DatabaseWidget::lock() if (m_db->isModified()) { bool saved = false; // Attempt to save on exit, but don't block locking if it fails - if (config()->get("AutoSaveOnExit").toBool()) { + if (config()->get("AutoSaveOnExit").toBool() || config()->get("AutoSaveAfterEveryChange").toBool()) { saved = save(); } @@ -1432,7 +1439,8 @@ bool DatabaseWidget::lock() void DatabaseWidget::reloadDatabaseFile() { - if (!m_db || isLocked()) { + // Ignore reload if we are locked or currently editing an entry or group + if (!m_db || isLocked() || isEntryEditActive() || isGroupEditActive()) { return; } @@ -1452,6 +1460,11 @@ void DatabaseWidget::reloadDatabaseFile() } } + // Lock out interactions + m_entryView->setDisabled(true); + m_groupView->setDisabled(true); + QApplication::processEvents(); + QString error; auto db = QSharedPointer::create(m_db->filePath()); if (db->open(database()->key(), &error)) { @@ -1491,6 +1504,10 @@ void DatabaseWidget::reloadDatabaseFile() // Mark db as modified since existing data may differ from file or file was deleted m_db->markAsModified(); } + + // Return control + m_entryView->setDisabled(false); + m_groupView->setDisabled(false); } int DatabaseWidget::numberOfSelectedEntries() const @@ -1631,11 +1648,20 @@ bool DatabaseWidget::save() m_blockAutoSave = true; ++m_saveAttempts; - // TODO: Make this async, but lock out the database widget to prevent re-entrance + // TODO: Make this async + // Lock out interactions + m_entryView->setDisabled(true); + m_groupView->setDisabled(true); + QApplication::processEvents(); + bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool(); QString errorMessage; bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get("BackupBeforeSave").toBool()); + // Return control + m_entryView->setDisabled(false); + m_groupView->setDisabled(false); + if (ok) { m_saveAttempts = 0; m_blockAutoSave = false; diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index ca5488e118..6420a3b242 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -82,6 +82,7 @@ class DatabaseWidget : public QStackedWidget bool isLocked() const; bool isSearchActive() const; bool isEntryEditActive() const; + bool isGroupEditActive() const; QString getCurrentSearch(); void refreshSearch(); diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 5102d196fe..822d40ba15 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -60,7 +60,7 @@ SearchWidget::SearchWidget(QWidget* parent) .arg(QKeySequence(QKeySequence::Find).toString(QKeySequence::NativeText))); m_ui->searchEdit->installEventFilter(this); - m_searchMenu = new QMenu(); + m_searchMenu = new QMenu(this); m_actionCaseSensitive = m_searchMenu->addAction(tr("Case sensitive"), this, SLOT(updateCaseSensitive())); m_actionCaseSensitive->setObjectName("actionSearchCaseSensitive"); m_actionCaseSensitive->setCheckable(true); diff --git a/src/gui/URLEdit.cpp b/src/gui/URLEdit.cpp new file mode 100644 index 0000000000..4dc2a55c27 --- /dev/null +++ b/src/gui/URLEdit.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 Felix Geyer + * Copyright (C) 2019 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 "URLEdit.h" + +#include + +#include "core/Config.h" +#include "core/FilePath.h" +#include "core/Tools.h" +#include "gui/Font.h" + +const QColor URLEdit::ErrorColor = QColor(255, 125, 125); + +URLEdit::URLEdit(QWidget* parent) + : QLineEdit(parent) +{ + const QIcon errorIcon = filePath()->icon("status", "dialog-error"); + m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition); + m_errorAction->setVisible(false); + m_errorAction->setToolTip(tr("Invalid URL")); + + updateStylesheet(); +} + +void URLEdit::enableVerifyMode() +{ + updateStylesheet(); + + connect(this, SIGNAL(textChanged(QString)), SLOT(updateStylesheet())); +} + +void URLEdit::updateStylesheet() +{ + const QString stylesheetTemplate("QLineEdit { background: %1; }"); + + if (!Tools::checkUrlValid(text())) { + setStyleSheet(stylesheetTemplate.arg(ErrorColor.name())); + m_errorAction->setVisible(true); + } else { + m_errorAction->setVisible(false); + setStyleSheet(""); + } +} diff --git a/src/gui/URLEdit.h b/src/gui/URLEdit.h new file mode 100644 index 0000000000..11b743b41e --- /dev/null +++ b/src/gui/URLEdit.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 Felix Geyer + * Copyright (C) 2019 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 KEEPASSX_URLEDIT_H +#define KEEPASSX_URLEDIT_H + +#include +#include +#include + +class URLEdit : public QLineEdit +{ + Q_OBJECT + +public: + static const QColor ErrorColor; + + explicit URLEdit(QWidget* parent = nullptr); + void enableVerifyMode(); + +private slots: + void updateStylesheet(); + +private: + QPointer m_errorAction; +}; + +#endif // KEEPASSX_URLEDIT_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 3fa75e3b8e..02b9f3a59c 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -167,6 +167,7 @@ void EditEntryWidget::setupMain() #ifdef WITH_XC_NETWORKING connect(m_mainUi->fetchFaviconButton, SIGNAL(clicked()), m_iconsWidget, SLOT(downloadFavicon())); connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), m_iconsWidget, SLOT(setUrl(QString))); + m_mainUi->urlEdit->enableVerifyMode(); #endif connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool))); @@ -201,7 +202,7 @@ void EditEntryWidget::setupAdvanced() connect(m_advancedUi->editAttributeButton, SIGNAL(clicked()), SLOT(editCurrentAttribute())); connect(m_advancedUi->removeAttributeButton, SIGNAL(clicked()), SLOT(removeCurrentAttribute())); connect(m_advancedUi->protectAttributeButton, SIGNAL(toggled(bool)), SLOT(protectCurrentAttribute(bool))); - connect(m_advancedUi->revealAttributeButton, SIGNAL(clicked(bool)), SLOT(revealCurrentAttribute())); + connect(m_advancedUi->revealAttributeButton, SIGNAL(clicked(bool)), SLOT(toggleCurrentAttributeVisibility())); connect(m_advancedUi->attributesView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(updateCurrentAttribute())); @@ -271,9 +272,14 @@ void EditEntryWidget::setupBrowser() m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes); m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel); + // Use a custom item delegate to align the icon to the right side + auto iconDelegate = new URLModelIconDelegate(m_browserUi->additionalURLsView); + m_browserUi->additionalURLsView->setItemDelegate(iconDelegate); + // clang-format off connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); + connect(m_browserUi->onlyHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->addURLButton, SIGNAL(clicked()), SLOT(insertURL())); connect(m_browserUi->removeURLButton, SIGNAL(clicked()), SLOT(removeCurrentURL())); connect(m_browserUi->editURLButton, SIGNAL(clicked()), SLOT(editCurrentURL())); @@ -300,8 +306,10 @@ void EditEntryWidget::updateBrowser() auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked(); auto hide = m_browserUi->hideEntryCheckbox->isChecked(); - m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? QString("true") : QString("false"))); - m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? QString("true") : QString("false"))); + auto onlyHttpAuth = m_browserUi->onlyHttpAuthCheckbox->isChecked(); + m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? TRUE_STR : FALSE_STR)); + m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? TRUE_STR : FALSE_STR)); + m_customData->set(BrowserService::OPTION_ONLY_HTTP_AUTH, (onlyHttpAuth ? TRUE_STR : FALSE_STR)); } void EditEntryWidget::insertURL() @@ -465,6 +473,7 @@ void EditEntryWidget::setupEntryUpdate() if (config()->get("Browser/Enabled", false).toBool()) { connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); + connect(m_browserUi->onlyHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->addURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->removeURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->editURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); @@ -959,18 +968,25 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) #ifdef WITH_XC_BROWSER if (m_customData->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) { // clang-format off - m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == "true"); + m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == TRUE_STR); // clang-format on } else { m_browserUi->skipAutoSubmitCheckbox->setChecked(false); } if (m_customData->contains(BrowserService::OPTION_HIDE_ENTRY)) { - m_browserUi->hideEntryCheckbox->setChecked(m_customData->value(BrowserService::OPTION_HIDE_ENTRY) == "true"); + m_browserUi->hideEntryCheckbox->setChecked(m_customData->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR); } else { m_browserUi->hideEntryCheckbox->setChecked(false); } + if (m_customData->contains(BrowserService::OPTION_ONLY_HTTP_AUTH)) { + m_browserUi->onlyHttpAuthCheckbox->setChecked(m_customData->value(BrowserService::OPTION_ONLY_HTTP_AUTH) + == TRUE_STR); + } else { + m_browserUi->onlyHttpAuthCheckbox->setChecked(false); + } + m_browserUi->addURLButton->setEnabled(!m_history); m_browserUi->removeURLButton->setEnabled(false); m_browserUi->editURLButton->setEnabled(false); @@ -1297,6 +1313,7 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) // Block signals to prevent modified being set m_advancedUi->protectAttributeButton->blockSignals(true); m_advancedUi->attributesEdit->blockSignals(true); + m_advancedUi->revealAttributeButton->setText(tr("Reveal")); if (index.isValid()) { QString key = m_attributesModel->keyByIndex(index); @@ -1348,7 +1365,7 @@ void EditEntryWidget::protectCurrentAttribute(bool state) } } -void EditEntryWidget::revealCurrentAttribute() +void EditEntryWidget::toggleCurrentAttributeVisibility() { if (!m_advancedUi->attributesEdit->isEnabled()) { QModelIndex index = m_advancedUi->attributesView->currentIndex(); @@ -1359,6 +1376,10 @@ void EditEntryWidget::revealCurrentAttribute() m_advancedUi->attributesEdit->setEnabled(true); m_advancedUi->attributesEdit->blockSignals(oldBlockSignals); } + m_advancedUi->revealAttributeButton->setText(tr("Hide")); + } else { + protectCurrentAttribute(true); + m_advancedUi->revealAttributeButton->setText(tr("Reveal")); } } diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 39b5fc5d96..300220cd0b 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -93,7 +93,7 @@ private slots: void removeCurrentAttribute(); void updateCurrentAttribute(); void protectCurrentAttribute(bool state); - void revealCurrentAttribute(); + void toggleCurrentAttributeVisibility(); void updateAutoTypeEnabled(); void openAutotypeHelp(); void insertAutoTypeAssoc(); diff --git a/src/gui/entry/EditEntryWidgetBrowser.ui b/src/gui/entry/EditEntryWidgetBrowser.ui index 4d0d29cf72..9d1e0f5117 100644 --- a/src/gui/entry/EditEntryWidgetBrowser.ui +++ b/src/gui/entry/EditEntryWidgetBrowser.ui @@ -50,6 +50,16 @@ + + + + Only send this setting to the browser for HTTP Auth dialogs. If enabled, normal login forms will not show this entry for selection. + + + Use this entry only with HTTP Basic Auth + + + @@ -130,6 +140,7 @@ skipAutoSubmitCheckbox hideEntryCheckbox + onlyHttpAuthCheckbox additionalURLsView addURLButton removeURLButton diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index 255cd0ab2f..54140fcd9f 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -30,7 +30,7 @@ - + Url field @@ -256,6 +256,12 @@
gui/PasswordEdit.h
1 + + URLEdit + QLineEdit +
gui/URLEdit.h
+ 1 +
titleEdit diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp index 3667c78f0c..3e6fb839c9 100644 --- a/src/gui/entry/EntryURLModel.cpp +++ b/src/gui/entry/EntryURLModel.cpp @@ -19,6 +19,7 @@ #include "EntryURLModel.h" #include "core/Entry.h" +#include "core/FilePath.h" #include "core/Tools.h" #include @@ -26,6 +27,7 @@ EntryURLModel::EntryURLModel(QObject* parent) : QStandardItemModel(parent) , m_entryAttributes(nullptr) + , m_errorIcon(filePath()->icon("status", "dialog-error")) { } @@ -53,6 +55,33 @@ void EntryURLModel::setEntryAttributes(EntryAttributes* entryAttributes) endResetModel(); } +QVariant EntryURLModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const auto key = keyByIndex(index); + if (key.isEmpty()) { + return {}; + } + + const auto value = m_entryAttributes->value(key); + const auto urlValid = Tools::checkUrlValid(value); + + if (role == Qt::BackgroundRole && !urlValid) { + return QColor(255, 125, 125); + } else if (role == Qt::DecorationRole && !urlValid) { + return m_errorIcon; + } else if (role == Qt::DisplayRole || role == Qt::EditRole) { + return value; + } else if (role == Qt::ToolTipRole && !urlValid) { + return tr("Invalid URL"); + } + + return {}; +} + bool EntryURLModel::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid() || role != Qt::EditRole || value.type() != QVariant::String || value.toString().isEmpty()) { diff --git a/src/gui/entry/EntryURLModel.h b/src/gui/entry/EntryURLModel.h index 09344d92af..f9ffa48288 100644 --- a/src/gui/entry/EntryURLModel.h +++ b/src/gui/entry/EntryURLModel.h @@ -20,9 +20,23 @@ #define KEEPASSXC_ENTRYURLMODEL_H #include +#include class EntryAttributes; +class URLModelIconDelegate : public QStyledItemDelegate +{ +public: + using QStyledItemDelegate::QStyledItemDelegate; + +protected: + void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override + { + QStyledItemDelegate::initStyleOption(option, index); + option->decorationPosition = QStyleOptionViewItem::Right; + } +}; + class EntryURLModel : public QStandardItemModel { Q_OBJECT @@ -32,6 +46,7 @@ class EntryURLModel : public QStandardItemModel void setEntryAttributes(EntryAttributes* entryAttributes); void insertRow(const QString& key, const QString& value); bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + QVariant data(const QModelIndex& index, int role) const override; QModelIndex indexByKey(const QString& key) const; QString keyByIndex(const QModelIndex& index) const; @@ -41,6 +56,7 @@ private slots: private: QList> m_urls; EntryAttributes* m_entryAttributes; + QIcon m_errorIcon; }; #endif // KEEPASSXC_ENTRYURLMODEL_H diff --git a/src/gui/macutils/AppKitImpl.mm b/src/gui/macutils/AppKitImpl.mm index 44137ee7f8..4a93f963a9 100644 --- a/src/gui/macutils/AppKitImpl.mm +++ b/src/gui/macutils/AppKitImpl.mm @@ -20,11 +20,6 @@ #import #import -#import - -#if __MAC_OS_X_VERSION_MAX_ALLOWED < 101200 -static const NSEventMask NSEventMaskKeyDown = NSKeyDownMask; -#endif @implementation AppKitImpl diff --git a/src/gui/widgets/ElidedLabel.cpp b/src/gui/widgets/ElidedLabel.cpp index bc27717640..749f075c8d 100644 --- a/src/gui/widgets/ElidedLabel.cpp +++ b/src/gui/widgets/ElidedLabel.cpp @@ -105,8 +105,10 @@ void ElidedLabel::updateElidedText() const QFontMetrics metrix(font()); displayText = metrix.elidedText(m_rawText, m_elideMode, width() - 2); } - setText(m_url.isEmpty() ? displayText : htmlLinkTemplate.arg(m_url, displayText)); - setOpenExternalLinks(!m_url.isEmpty()); + + bool hasUrl = !m_url.isEmpty(); + setText(hasUrl ? htmlLinkTemplate.arg(m_url.toHtmlEscaped(), displayText) : displayText); + setOpenExternalLinks(!hasUrl); } void ElidedLabel::resizeEvent(QResizeEvent* event) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a89cfb2195..443a0e19d1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -164,7 +164,7 @@ endif() if(WITH_XC_CRYPTO_SSH) add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp - LIBS ${TEST_LIBRARIES}) + LIBS ${TEST_LIBRARIES}) endif() add_unit_test(NAME testentry SOURCES TestEntry.cpp @@ -195,7 +195,7 @@ add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testrandomgenerator SOURCES TestRandomGenerator.cpp - LIBS testsupport ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp LIBS ${TEST_LIBRARIES}) @@ -209,7 +209,7 @@ add_unit_test(NAME testykchallengeresponsekey if(WITH_XC_KEESHARE) add_unit_test(NAME testsharing SOURCES TestSharing.cpp - LIBS testsupport ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) endif() add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index 818dfaebdf..8da2a28964 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -18,6 +18,7 @@ #include "TestBrowser.h" #include "TestGlobal.h" #include "browser/BrowserSettings.h" +#include "core/Tools.h" #include "crypto/Crypto.h" #include "sodium/crypto_box.h" #include @@ -56,7 +57,7 @@ void TestBrowser::testChangePublicKeys() auto response = m_browserAction->handleAction(json); QCOMPARE(response["action"].toString(), QString("change-public-keys")); QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false); - QCOMPARE(response["success"].toString(), QString("true")); + QCOMPARE(response["success"].toString(), TRUE_STR); } void TestBrowser::testEncryptMessage() @@ -179,29 +180,22 @@ void TestBrowser::testSearchEntries() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QList urls; - urls.push_back("https://github.com/login_page"); - urls.push_back("https://github.com/login"); - urls.push_back("https://github.com/"); - urls.push_back("github.com/login"); - urls.push_back("http://github.com"); - urls.push_back("http://github.com/login"); - urls.push_back("github.com"); - urls.push_back("github.com/login"); - urls.push_back("https://github"); // Invalid URL - urls.push_back("github.com"); + QStringList urls = {"https://github.com/login_page", + "https://github.com/login", + "https://github.com/", + "github.com/login", + "http://github.com", + "http://github.com/login", + "github.com", + "github.com/login", + "https://github", // Invalid URL + "github.com"}; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - } + createEntries(urls, root); browserSettings()->setMatchUrlScheme(false); - auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + auto result = + m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl QCOMPARE(result.length(), 9); QCOMPARE(result[0]->url(), QString("https://github.com/login_page")); @@ -213,7 +207,7 @@ void TestBrowser::testSearchEntries() // With matching there should be only 3 results + 4 without a scheme browserSettings()->setMatchUrlScheme(true); - result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); QCOMPARE(result.length(), 7); QCOMPARE(result[0]->url(), QString("https://github.com/login_page")); QCOMPARE(result[1]->url(), QString("https://github.com/login")); @@ -226,22 +220,13 @@ void TestBrowser::testSearchEntriesWithPort() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QList urls; - urls.push_back("http://127.0.0.1:443"); - urls.push_back("http://127.0.0.1:80"); + QStringList urls = {"http://127.0.0.1:443", "http://127.0.0.1:80"}; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - } + createEntries(urls, root); - auto result = m_browserService->searchEntries(db, "127.0.0.1", "http://127.0.0.1:443"); // db, hostname, url + auto result = m_browserService->searchEntries(db, "http://127.0.0.1:443", "http://127.0.0.1"); QCOMPARE(result.length(), 1); - QCOMPARE(result[0]->url(), urls[0]); + QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443")); } void TestBrowser::testSearchEntriesWithAdditionalURLs() @@ -249,70 +234,55 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QList entries; - QList urls; - urls.push_back("https://github.com/"); - urls.push_back("https://www.example.com"); - urls.push_back("http://domain.com"); + QStringList urls = {"https://github.com/", "https://www.example.com", "http://domain.com"}; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - entries.push_back(entry); - } + auto entries = createEntries(urls, root); // Add an additional URL to the first entry entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org"); - auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); QCOMPARE(result.length(), 1); - QCOMPARE(result[0]->url(), urls[0]); + QCOMPARE(result[0]->url(), QString("https://github.com/")); // Search the additional URL. It should return the same entry - auto additionalResult = m_browserService->searchEntries(db, "keepassxc.org", "https://keepassxc.org"); + auto additionalResult = m_browserService->searchEntries(db, "https://keepassxc.org", "https://keepassxc.org"); QCOMPARE(additionalResult.length(), 1); - QCOMPARE(additionalResult[0]->url(), urls[0]); + QCOMPARE(additionalResult[0]->url(), QString("https://github.com/")); } void TestBrowser::testInvalidEntries() { auto db = QSharedPointer::create(); auto* root = db->rootGroup(); + const QString url("https://github.com"); + const QString submitUrl("https://github.com/session"); - QList urls; - urls.push_back("https://github.com/login"); - urls.push_back("https:///github.com/"); // Extra '/' - urls.push_back("http://github.com/**//*"); - urls.push_back("http://*.github.com/login"); - urls.push_back("//github.com"); // fromUserInput() corrects this one. - urls.push_back("github.com/{}<>"); + QStringList urls = { + "https://github.com/login", + "https:///github.com/", // Extra '/' + "http://github.com/**//*", + "http://*.github.com/login", + "//github.com", // fromUserInput() corrects this one. + "github.com/{}<>", + "http:/example.com", + }; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - } + createEntries(urls, root); browserSettings()->setMatchUrlScheme(true); - auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); QCOMPARE(result.length(), 2); QCOMPARE(result[0]->url(), QString("https://github.com/login")); QCOMPARE(result[1]->url(), QString("//github.com")); // Test the URL's directly - QCOMPARE(m_browserService->handleURL(urls[0], "github.com", "https://github.com"), true); - QCOMPARE(m_browserService->handleURL(urls[1], "github.com", "https://github.com"), false); - QCOMPARE(m_browserService->handleURL(urls[2], "github.com", "https://github.com"), false); - QCOMPARE(m_browserService->handleURL(urls[3], "github.com", "https://github.com"), false); - QCOMPARE(m_browserService->handleURL(urls[4], "github.com", "https://github.com"), true); - QCOMPARE(m_browserService->handleURL(urls[5], "github.com", "https://github.com"), false); + QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true); + QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false); + QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false); + QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false); + QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true); + QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false); } void TestBrowser::testSubdomainsAndPaths() @@ -320,44 +290,74 @@ void TestBrowser::testSubdomainsAndPaths() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QList urls; - urls.push_back("https://www.github.com/login/page.xml"); - urls.push_back("https://login.github.com/"); - urls.push_back("https://github.com"); - urls.push_back("http://www.github.com"); - urls.push_back("http://login.github.com/pathtonowhere"); - urls.push_back(".github.com"); // Invalid URL - urls.push_back("www.github.com/"); - urls.push_back("https://github"); // Invalid URL + QStringList urls = { + "https://www.github.com/login/page.xml", + "https://login.github.com/", + "https://github.com", + "http://www.github.com", + "http://login.github.com/pathtonowhere", + ".github.com", // Invalid URL + "www.github.com/", + "https://github" // Invalid URL + }; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - } + createEntries(urls, root); browserSettings()->setMatchUrlScheme(false); - auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); + QCOMPARE(result.length(), 1); + QCOMPARE(result[0]->url(), QString("https://github.com")); - QCOMPARE(result.length(), 6); - QCOMPARE(result[0]->url(), urls[0]); - QCOMPARE(result[1]->url(), urls[1]); - QCOMPARE(result[2]->url(), urls[2]); - QCOMPARE(result[3]->url(), urls[3]); - QCOMPARE(result[4]->url(), urls[4]); - QCOMPARE(result[5]->url(), urls[6]); + // With www subdomain + result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session"); + QCOMPARE(result.length(), 4); + QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml")); + QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain + QCOMPARE(result[2]->url(), QString("http://www.github.com")); + QCOMPARE(result[3]->url(), QString("www.github.com/")); - // With matching there should be only 3 results + // With scheme matching there should be only 1 result browserSettings()->setMatchUrlScheme(true); - result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url + result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); + QCOMPARE(result.length(), 1); + QCOMPARE(result[0]->url(), QString("https://github.com")); + + // Test site with subdomain in the site URL + QStringList entryURLs = { + "https://accounts.example.com", + "https://accounts.example.com/path", + "https://subdomain.example.com/", + "https://another.accounts.example.com/", + "https://another.subdomain.example.com/", + "https://example.com/", + "https://example" // Invalid URL + }; + + createEntries(entryURLs, root); + + result = m_browserService->searchEntries(db, "https://accounts.example.com", "https://accounts.example.com"); + QCOMPARE(result.length(), 3); + QCOMPARE(result[0]->url(), QString("https://accounts.example.com")); + QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path")); + QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain + + result = m_browserService->searchEntries( + db, "https://another.accounts.example.com", "https://another.accounts.example.com"); QCOMPARE(result.length(), 4); - QCOMPARE(result[0]->url(), urls[0]); - QCOMPARE(result[1]->url(), urls[1]); - QCOMPARE(result[2]->url(), urls[2]); - QCOMPARE(result[3]->url(), urls[6]); + QCOMPARE(result[0]->url(), + QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com + QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path")); + QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/")); + QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains + + // Test local files. It should be a direct match. + QStringList localFiles = {"file:///Users/testUser/tests/test.html"}; + + createEntries(localFiles, root); + + // With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL. + result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html"); + QCOMPARE(result.length(), 1); } void TestBrowser::testSortEntries() @@ -365,28 +365,18 @@ void TestBrowser::testSortEntries() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QList urls; - urls.push_back("https://github.com/login_page"); - urls.push_back("https://github.com/login"); - urls.push_back("https://github.com/"); - urls.push_back("github.com/login"); - urls.push_back("http://github.com"); - urls.push_back("http://github.com/login"); - urls.push_back("github.com"); - urls.push_back("github.com/login"); - urls.push_back("https://github"); // Invalid URL - urls.push_back("github.com"); + QStringList urls = {"https://github.com/login_page", + "https://github.com/login", + "https://github.com/", + "github.com/login", + "http://github.com", + "http://github.com/login", + "github.com", + "github.com/login", + "https://github", // Invalid URL + "github.com"}; - QList entries; - for (int i = 0; i < urls.length(); ++i) { - auto entry = new Entry(); - entry->setGroup(root); - entry->beginUpdate(); - entry->setUrl(urls[i]); - entry->setUsername(QString("User %1").arg(i)); - entry->endUpdate(); - entries.push_back(entry); - } + auto entries = createEntries(urls, root); browserSettings()->setBestMatchOnly(false); auto result = @@ -457,3 +447,38 @@ void TestBrowser::testGetDatabaseGroups() auto lastChild = lastChildren.at(0); QCOMPARE(lastChild.toObject()["name"].toString(), QString("group2_1_1")); } + +QList TestBrowser::createEntries(QStringList& urls, Group* root) const +{ + QList entries; + for (int i = 0; i < urls.length(); ++i) { + auto entry = new Entry(); + entry->setGroup(root); + entry->beginUpdate(); + entry->setUrl(urls[i]); + entry->setUsername(QString("User %1").arg(i)); + entry->endUpdate(); + entries.push_back(entry); + } + + return entries; +} +void TestBrowser::testValidURLs() +{ + QHash urls; + urls["https://github.com/login"] = true; + urls["https:///github.com/"] = false; + urls["http://github.com/**//*"] = false; + urls["http://*.github.com/login"] = false; + urls["//github.com"] = true; + urls["github.com/{}<>"] = false; + urls["http:/example.com"] = false; + urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true; + urls["file:///Users/testUser/Code/test.html"] = true; + + QHashIterator i(urls); + while (i.hasNext()) { + i.next(); + QCOMPARE(Tools::checkUrlValid(i.key()), i.value()); + } +} diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index 981c1642d1..69ba693095 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -47,8 +47,11 @@ private slots: void testSubdomainsAndPaths(); void testSortEntries(); void testGetDatabaseGroups(); + void testValidURLs(); private: + QList createEntries(QStringList& urls, Group* root) const; + QScopedPointer m_browserAction; QScopedPointer m_browserService; }; diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 9a2756eac2..076f7f74ed 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -1422,6 +1422,107 @@ void TestCli::testMerge() QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); } +void TestCli::testMergeWithKeys() +{ + Create createCmd; + QVERIFY(!createCmd.name.isEmpty()); + QVERIFY(createCmd.getDescriptionLine().contains(createCmd.name)); + + Merge mergeCmd; + QVERIFY(!mergeCmd.name.isEmpty()); + QVERIFY(mergeCmd.getDescriptionLine().contains(mergeCmd.name)); + + Kdbx4Writer writer; + Kdbx4Reader reader; + + QScopedPointer testDir(new QTemporaryDir()); + + QString sourceDatabaseFilename = testDir->path() + "/testSourceDatabase.kdbx"; + QString sourceKeyfilePath = testDir->path() + "/testSourceKeyfile.txt"; + + QString targetDatabaseFilename = testDir->path() + "/testTargetDatabase.kdbx"; + QString targetKeyfilePath = testDir->path() + "/testTargetKeyfile.txt"; + + qint64 pos = m_stdoutFile->pos(); + + Utils::Test::setNextPassword("a"); + createCmd.execute({"create", sourceDatabaseFilename, "-k", sourceKeyfilePath}); + + Utils::Test::setNextPassword("b"); + createCmd.execute({"create", targetDatabaseFilename, "-k", targetKeyfilePath}); + + Utils::Test::setNextPassword("a"); + auto sourceDatabase = QSharedPointer( + Utils::unlockDatabase(sourceDatabaseFilename, true, sourceKeyfilePath, "", Utils::STDOUT)); + QVERIFY(sourceDatabase); + + Utils::Test::setNextPassword("b"); + auto targetDatabase = QSharedPointer( + Utils::unlockDatabase(targetDatabaseFilename, true, targetKeyfilePath, "", Utils::STDOUT)); + QVERIFY(targetDatabase); + + auto* rootGroup = new Group(); + rootGroup->setName("root"); + rootGroup->setUuid(QUuid::createUuid()); + auto* group = new Group(); + group->setUuid(QUuid::createUuid()); + group->setParent(rootGroup); + group->setName("Internet"); + + auto* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle("Some Website"); + entry->setPassword("secretsecretsecret"); + group->addEntry(entry); + + sourceDatabase->setRootGroup(rootGroup); + + auto* otherRootGroup = new Group(); + otherRootGroup->setName("root"); + otherRootGroup->setUuid(QUuid::createUuid()); + auto* otherGroup = new Group(); + otherGroup->setUuid(QUuid::createUuid()); + otherGroup->setParent(otherRootGroup); + otherGroup->setName("Internet"); + + auto* otherEntry = new Entry(); + otherEntry->setUuid(QUuid::createUuid()); + otherEntry->setTitle("Some Website 2"); + otherEntry->setPassword("secretsecretsecret 2"); + otherGroup->addEntry(otherEntry); + + targetDatabase->setRootGroup(otherRootGroup); + + QFile sourceDatabaseFile(sourceDatabaseFilename); + sourceDatabaseFile.open(QIODevice::WriteOnly); + QVERIFY(writer.writeDatabase(&sourceDatabaseFile, sourceDatabase.data())); + sourceDatabaseFile.flush(); + sourceDatabaseFile.close(); + + QFile targetDatabaseFile(targetDatabaseFilename); + targetDatabaseFile.open(QIODevice::WriteOnly); + QVERIFY(writer.writeDatabase(&targetDatabaseFile, targetDatabase.data())); + targetDatabaseFile.flush(); + targetDatabaseFile.close(); + + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("b"); + Utils::Test::setNextPassword("a"); + mergeCmd.execute({"merge", + "-k", + targetKeyfilePath, + "--key-file-from", + sourceKeyfilePath, + targetDatabaseFile.fileName(), + sourceDatabaseFile.fileName()}); + + m_stdoutFile->seek(pos); + QList lines = m_stdoutFile->readAll().split('\n'); + QVERIFY(lines.contains(QString("Successfully merged %1 into %2.") + .arg(sourceDatabaseFile.fileName(), targetDatabaseFile.fileName()) + .toUtf8())); +} + void TestCli::testMove() { Move moveCmd; diff --git a/tests/TestCli.h b/tests/TestCli.h index bd0f9fc3fe..4947ee472b 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -66,6 +66,7 @@ private slots: void testList(); void testLocate(); void testMerge(); + void testMergeWithKeys(); void testMove(); void testOpen(); void testRemove(); diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 9fc39dc64e..47a917e436 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -20,6 +20,7 @@ #include "TestGlobal.h" #include "mock/MockClock.h" +#include #include #include "core/Metadata.h" @@ -798,16 +799,16 @@ void TestGroup::testAddEntryWithPath() void TestGroup::testIsRecycled() { - Database* db = new Database(); - db->metadata()->setRecycleBinEnabled(true); + Database db; + db.metadata()->setRecycleBinEnabled(true); Group* group1 = new Group(); group1->setName("group1"); - group1->setParent(db->rootGroup()); + group1->setParent(db.rootGroup()); Group* group2 = new Group(); group2->setName("group2"); - group2->setParent(db->rootGroup()); + group2->setParent(db.rootGroup()); Group* group3 = new Group(); group3->setName("group3"); @@ -815,16 +816,16 @@ void TestGroup::testIsRecycled() Group* group4 = new Group(); group4->setName("group4"); - group4->setParent(db->rootGroup()); + group4->setParent(db.rootGroup()); - db->recycleGroup(group2); + db.recycleGroup(group2); QVERIFY(!group1->isRecycled()); QVERIFY(group2->isRecycled()); QVERIFY(group3->isRecycled()); QVERIFY(!group4->isRecycled()); - db->recycleGroup(group4); + db.recycleGroup(group4); QVERIFY(group4->isRecycled()); } @@ -1052,12 +1053,12 @@ void TestGroup::testChildrenSort() void TestGroup::testHierarchy() { - Group* group1 = new Group(); - group1->setName("group1"); + Group group1; + group1.setName("group1"); Group* group2 = new Group(); group2->setName("group2"); - group2->setParent(group1); + group2->setParent(&group1); Group* group3 = new Group(); group3->setName("group3"); @@ -1085,11 +1086,11 @@ void TestGroup::testHierarchy() void TestGroup::testApplyGroupIconRecursively() { // Create a database with two nested groups with one entry each - Database* database = new Database(); + Database database; Group* subgroup = new Group(); subgroup->setName("Subgroup"); - subgroup->setParent(database->rootGroup()); + subgroup->setParent(database.rootGroup()); QVERIFY(subgroup); Group* subsubgroup = new Group(); @@ -1108,10 +1109,10 @@ void TestGroup::testApplyGroupIconRecursively() // Set an icon per number to the root group and apply recursively // -> all groups and entries have the same icon const int rootIconNumber = 42; - database->rootGroup()->setIcon(rootIconNumber); - QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber); - database->rootGroup()->applyGroupIconToChildGroups(); - database->rootGroup()->applyGroupIconToChildEntries(); + database.rootGroup()->setIcon(rootIconNumber); + QVERIFY(database.rootGroup()->iconNumber() == rootIconNumber); + database.rootGroup()->applyGroupIconToChildGroups(); + database.rootGroup()->applyGroupIconToChildEntries(); QVERIFY(subgroup->iconNumber() == rootIconNumber); QVERIFY(subgroupEntry->iconNumber() == rootIconNumber); QVERIFY(subsubgroup->iconNumber() == rootIconNumber); @@ -1124,7 +1125,7 @@ void TestGroup::testApplyGroupIconRecursively() QVERIFY(subsubgroup->iconNumber() == subsubgroupIconNumber); subsubgroup->applyGroupIconToChildGroups(); subsubgroup->applyGroupIconToChildEntries(); - QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber); + QVERIFY(database.rootGroup()->iconNumber() == rootIconNumber); QVERIFY(subgroup->iconNumber() == rootIconNumber); QVERIFY(subgroupEntry->iconNumber() == rootIconNumber); QVERIFY(subsubgroup->iconNumber() == subsubgroupIconNumber); @@ -1135,11 +1136,11 @@ void TestGroup::testApplyGroupIconRecursively() const QUuid subgroupIconUuid = QUuid::createUuid(); QImage subgroupIcon(16, 16, QImage::Format_RGB32); subgroupIcon.setPixel(0, 0, qRgb(255, 0, 0)); - database->metadata()->addCustomIcon(subgroupIconUuid, subgroupIcon); + database.metadata()->addCustomIcon(subgroupIconUuid, subgroupIcon); subgroup->setIcon(subgroupIconUuid); subgroup->applyGroupIconToChildGroups(); subgroup->applyGroupIconToChildEntries(); - QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber); + QVERIFY(database.rootGroup()->iconNumber() == rootIconNumber); QCOMPARE(subgroup->iconUuid(), subgroupIconUuid); QCOMPARE(subgroup->icon(), subgroupIcon); QCOMPARE(subgroupEntry->iconUuid(), subgroupIconUuid); @@ -1150,10 +1151,10 @@ void TestGroup::testApplyGroupIconRecursively() QCOMPARE(subsubgroupEntry->icon(), subgroupIcon); // Reset all icons to root icon - database->rootGroup()->setIcon(rootIconNumber); - QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber); - database->rootGroup()->applyGroupIconToChildGroups(); - database->rootGroup()->applyGroupIconToChildEntries(); + database.rootGroup()->setIcon(rootIconNumber); + QVERIFY(database.rootGroup()->iconNumber() == rootIconNumber); + database.rootGroup()->applyGroupIconToChildGroups(); + database.rootGroup()->applyGroupIconToChildEntries(); QVERIFY(subgroup->iconNumber() == rootIconNumber); QVERIFY(subgroupEntry->iconNumber() == rootIconNumber); QVERIFY(subsubgroup->iconNumber() == rootIconNumber); @@ -1161,10 +1162,10 @@ void TestGroup::testApplyGroupIconRecursively() // Apply only for child groups const int iconForGroups = 10; - database->rootGroup()->setIcon(iconForGroups); - QVERIFY(database->rootGroup()->iconNumber() == iconForGroups); - database->rootGroup()->applyGroupIconToChildGroups(); - QVERIFY(database->rootGroup()->iconNumber() == iconForGroups); + database.rootGroup()->setIcon(iconForGroups); + QVERIFY(database.rootGroup()->iconNumber() == iconForGroups); + database.rootGroup()->applyGroupIconToChildGroups(); + QVERIFY(database.rootGroup()->iconNumber() == iconForGroups); QVERIFY(subgroup->iconNumber() == iconForGroups); QVERIFY(subgroupEntry->iconNumber() == rootIconNumber); QVERIFY(subsubgroup->iconNumber() == iconForGroups); @@ -1172,10 +1173,10 @@ void TestGroup::testApplyGroupIconRecursively() // Apply only for child entries const int iconForEntries = 20; - database->rootGroup()->setIcon(iconForEntries); - QVERIFY(database->rootGroup()->iconNumber() == iconForEntries); - database->rootGroup()->applyGroupIconToChildEntries(); - QVERIFY(database->rootGroup()->iconNumber() == iconForEntries); + database.rootGroup()->setIcon(iconForEntries); + QVERIFY(database.rootGroup()->iconNumber() == iconForEntries); + database.rootGroup()->applyGroupIconToChildEntries(); + QVERIFY(database.rootGroup()->iconNumber() == iconForEntries); QVERIFY(subgroup->iconNumber() == iconForGroups); QVERIFY(subgroupEntry->iconNumber() == iconForEntries); QVERIFY(subsubgroup->iconNumber() == iconForGroups); @@ -1184,15 +1185,15 @@ void TestGroup::testApplyGroupIconRecursively() void TestGroup::testUsernamesRecursive() { - Database* database = new Database(); + Database database; // Create a subgroup Group* subgroup = new Group(); subgroup->setName("Subgroup"); - subgroup->setParent(database->rootGroup()); + subgroup->setParent(database.rootGroup()); // Generate entries in the root group and the subgroup - Entry* rootGroupEntry = database->rootGroup()->addEntryWithPath("Root group entry"); + Entry* rootGroupEntry = database.rootGroup()->addEntryWithPath("Root group entry"); rootGroupEntry->setUsername("Name1"); Entry* subgroupEntry = subgroup->addEntryWithPath("Subgroup entry"); @@ -1201,7 +1202,7 @@ void TestGroup::testUsernamesRecursive() Entry* subgroupEntryReusingUsername = subgroup->addEntryWithPath("Another subgroup entry"); subgroupEntryReusingUsername->setUsername("Name2"); - QList usernames = database->rootGroup()->usernamesRecursive(); + QList usernames = database.rootGroup()->usernamesRecursive(); QCOMPARE(usernames.size(), 2); QVERIFY(usernames.contains("Name1")); QVERIFY(usernames.contains("Name2")); diff --git a/tests/TestOpVaultReader.cpp b/tests/TestOpVaultReader.cpp index af332fd322..15f30f2c96 100644 --- a/tests/TestOpVaultReader.cpp +++ b/tests/TestOpVaultReader.cpp @@ -49,24 +49,24 @@ QPair* split1PTextExportKV(QByteArray& line) return new QPair(k, v); } -QJsonArray* read1PasswordTextExport(QFile& f) +QSharedPointer read1PasswordTextExport(QFile& f) { - auto result = new QJsonArray; - auto current = new QJsonObject; - if (!f.open(QIODevice::ReadOnly)) { qCritical("Unable to open your text export file for reading"); - return nullptr; + return {}; } + auto result = QSharedPointer::create(); + QJsonObject current; + while (!f.atEnd()) { auto line = f.readLine(1024); if (line.size() == 1 and line[0] == '\n') { - if (!current->isEmpty()) { - result->append(*current); + if (!current.isEmpty()) { + result->append(current); } - current = new QJsonObject; + current = QJsonObject(); continue; } const auto kv = split1PTextExportKV(line); @@ -95,14 +95,14 @@ QJsonArray* read1PasswordTextExport(QFile& f) } } auto v = lines.join(""); - (*current)[k] = v; + current[k] = v; } else { - (*current)[k] = kv->second; + current[k] = kv->second; } delete kv; } - if (!current->isEmpty()) { - result->append(*current); + if (!current.isEmpty()) { + result->append(current); } f.close(); @@ -120,10 +120,9 @@ void TestOpVaultReader::initTestCase() m_password = "freddy"; QFile testData(m_opVaultTextExportPath); - QJsonArray* data = read1PasswordTextExport(testData); + auto data = read1PasswordTextExport(testData); QVERIFY(data); QCOMPARE(data->size(), 27); - delete data; m_categoryMap.insert("001", "Login"); m_categoryMap.insert("002", "Credit Card"); @@ -149,9 +148,9 @@ void TestOpVaultReader::testReadIntoDatabase() { QDir opVaultDir(m_opVaultPath); - auto reader = new OpVaultReader(); - auto db = reader->readDatabase(opVaultDir, m_password); - QVERIFY2(!reader->hasError(), qPrintable(reader->errorString())); + OpVaultReader reader; + QScopedPointer db(reader.readDatabase(opVaultDir, m_password)); + QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); QVERIFY(db); QVERIFY(!db->children().isEmpty()); @@ -179,7 +178,6 @@ void TestOpVaultReader::testReadIntoDatabase() QUuid u = Tools::hexToUuid(value["uuid"].toString()); objectsByUuid[u] = value; } - delete testData; QCOMPARE(objectsByUuid.size(), 27); for (QUuid u : objectsByUuid.keys()) { @@ -240,11 +238,11 @@ void TestOpVaultReader::testKeyDerivation() void TestOpVaultReader::testBandEntry1() { - auto reader = new OpVaultReader(); + OpVaultReader reader; QByteArray json(R"({"hello": "world"})"); QJsonDocument doc = QJsonDocument::fromJson(json); QJsonObject data; QByteArray entryKey; QByteArray entryHmacKey; - QVERIFY(!reader->decryptBandEntry(doc.object(), data, entryKey, entryHmacKey)); + QVERIFY(!reader.decryptBandEntry(doc.object(), data, entryKey, entryHmacKey)); }