diff --git a/CMakeLists.txt b/CMakeLists.txt index fd02b48..6971e64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,42 +1,42 @@ cmake_minimum_required(VERSION 3.16) set(VERSION "0.0.1") project(gnupgpass VERSION 0.0.1) -set(QT_MIN_VERSION "5.15.2") -set(KF_MIN_VERSION "5.102.0") +set(QT_MIN_VERSION "6.6.0") +set(KF_MIN_VERSION "5.240.0") find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(ECMMarkAsTest) include(ECMAddTests) include(FeatureSummary) include(ECMAddAppIcon) include(ECMSetupVersion) include(KDEGitCommitHooks) include(KDEClangFormat) # Generate header with version number ecm_setup_version(${VERSION} VARIABLE_PREFIX GPGPASS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/gpgpass_version.h") -find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test) +find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test) include_directories(${CMAKE_BINARY_DIR}) -find_package(KF5 ${KF_MIN_VERSION} REQUIRED COMPONENTS Prison IconThemes I18n ConfigWidgets WidgetsAddons) -find_package(QGpgme 1.18 CONFIG REQUIRED) +find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Prison IconThemes I18n ColorScheme WidgetsAddons) +find_package(QGpgmeQt6 1.19 CONFIG REQUIRED) add_subdirectory(main) add_subdirectory(src) add_subdirectory(tests) ki18n_install(po) install(FILES org.gnupg.gpgpass.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.gnupg.gpgpass.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES artwork/sc-gpgpass.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps RENAME org.gnupg.gpgpass.svg) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9288455..df9e4be 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,7 +1,7 @@ add_executable(gpgpass) -target_link_libraries(gpgpass gpgpass_internal KF5::IconThemes KF5::ConfigWidgets) +target_link_libraries(gpgpass gpgpass_internal KF6::IconThemes KF6::ColorScheme) ecm_add_app_icon(GPGPASS_ICONS ICONS ${CMAKE_SOURCE_DIR}/artwork/sc-gpgpass.svg ${CMAKE_SOURCE_DIR}/artwork/32-gpgpass.png ${CMAKE_SOURCE_DIR}/artwork/64-gpgpass.png ${CMAKE_SOURCE_DIR}/artwork/256-gpgpass.png) target_sources(gpgpass PRIVATE main.cpp ${GPGPASS_ICONS}) target_compile_definitions(gpgpass PRIVATE QT_NO_TRANSLATION) install(TARGETS gpgpass ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/main/main.cpp b/main/main.cpp index e44a412..967d8ea 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,72 +1,72 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "firsttimedialog.h" #include "mainwindow.h" #include #include #include #include #include -#include -#include -#include +#include +#include +#include /** * @brief main * @param argc * @param argv * @return */ #include int main(int argc, char *argv[]) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // allow darkmode #if defined(Q_OS_WIN) if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) { qputenv("QT_QPA_PLATFORM", "windows:darkmode=1"); } #endif QApplication app(argc, argv); KLocalizedString::setApplicationDomain("gpgpass"); Q_INIT_RESOURCE(resources); QCoreApplication::setOrganizationName(QStringLiteral("GnuPG")); QCoreApplication::setOrganizationDomain(QStringLiteral("gnupg.org")); QCoreApplication::setApplicationName(QStringLiteral("GnuPGPass")); QCoreApplication::setApplicationVersion(QString::fromUtf8(GPGPASS_VERSION_STRING)); QApplication::setWindowIcon(QIcon(QStringLiteral(":/artwork/64-gpgpass.png"))); QGuiApplication::setDesktopFileName(QStringLiteral("org.gnupg.gpgpass.desktop")); MainWindow w; // ensure KIconThemes is loaded for rcc icons KIconLoader::global()->hasIcon(QString{}); KColorSchemeManager m; FirstTimeDialog d(&w, w.pass()); QSettings s; if (!s.value(QStringLiteral("setup"), false).toBool()) { d.show(); } else { w.show(); } return app.exec(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee3b890..1a1a100 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,49 +1,49 @@ # let's put most of the "meat" in a static library this way, we can also unit # test parts of it add_library(gpgpass_internal STATIC) target_sources( gpgpass_internal PRIVATE clipboardhelper.h configdialog.h deselectabletreeview.h filecontent.h firsttimedialog.h gpgmehelpers.h mainwindow.h pass.h passwordconfiguration.h passworddialog.h qpushbuttonfactory.h settings.h storemodel.h userinfo.h usersdialog.h util.h clipboardhelper.cpp configdialog.cpp filecontent.cpp firsttimedialog.cpp mainwindow.cpp pass.cpp passworddialog.cpp settings.cpp storemodel.cpp usersdialog.cpp util.cpp ../resources.qrc ) ki18n_wrap_ui(gpgpass_internal mainwindow.ui configdialog.ui usersdialog.ui keygenwidget.ui passworddialog.ui userswidget.ui selectpasswordstore.ui ) -target_link_libraries(gpgpass_internal Qt5::Widgets KF5::Prison KF5::IconThemes KF5::I18n KF5::WidgetsAddons - QGpgme) +target_link_libraries(gpgpass_internal Qt6::Widgets KF6::Prison KF6::IconThemes KF6::I18n KF6::WidgetsAddons + QGpgmeQt6) diff --git a/src/clipboardhelper.cpp b/src/clipboardhelper.cpp index 53fb7ee..d93cc77 100644 --- a/src/clipboardhelper.cpp +++ b/src/clipboardhelper.cpp @@ -1,87 +1,87 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "clipboardhelper.h" #include "settings.h" #include #include #include #include #include #include #include #include -#include +#include ClipboardHelper::ClipboardHelper(QMainWindow *mainWindow) : QObject(mainWindow) , m_mainWindow(mainWindow) , clippedText(QString()) { setClipboardTimer(); clearClipboardTimer.setSingleShot(true); connect(&clearClipboardTimer, &QTimer::timeout, this, &ClipboardHelper::clearClipboard); } ClipboardHelper::~ClipboardHelper() = default; void ClipboardHelper::setClippedText(const QString &password) { clippedText = password; } void ClipboardHelper::clearClippedText() { clippedText = QString{}; } void ClipboardHelper::setClipboardTimer() { clearClipboardTimer.setInterval(1000 * Settings::getAutoclearSeconds()); } /** * Clears the clipboard if we filled it */ void ClipboardHelper::clearClipboard() { QClipboard *clipboard = QApplication::clipboard(); bool cleared = false; if (this->clippedText == clipboard->text(QClipboard::Selection)) { clipboard->clear(QClipboard::Selection); clipboard->setText(QString{}, QClipboard::Selection); cleared = true; } if (this->clippedText == clipboard->text(QClipboard::Clipboard)) { clipboard->clear(QClipboard::Clipboard); cleared = true; } if (cleared) { m_mainWindow->statusBar()->showMessage(i18n("Clipboard cleared"), 2000); } else { m_mainWindow->statusBar()->showMessage(i18n("Clipboard not cleared"), 2000); } clippedText.clear(); } /** * @brief MainWindow::copyTextToClipboard copies text to your clipboard * @param text */ void ClipboardHelper::copyTextToClipboard(const QString &text) { QClipboard *clip = QApplication::clipboard(); clip->setText(text, QClipboard::Clipboard); clippedText = text; m_mainWindow->statusBar()->showMessage(i18n("Copied to clipboard"), 2000); if (Settings::isUseAutoclear()) { clearClipboardTimer.start(); } } diff --git a/src/configdialog.cpp b/src/configdialog.cpp index e018b63..01817fb 100644 --- a/src/configdialog.cpp +++ b/src/configdialog.cpp @@ -1,320 +1,320 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "configdialog.h" #include "mainwindow.h" #include "settings.h" #include "ui_configdialog.h" #include "util.h" #include #include #include #include #include #include -#include +#include /** * @brief ConfigDialog::ConfigDialog this sets up the configuration screen. * @param parent */ ConfigDialog::ConfigDialog(MainWindow *parent) : QDialog(parent) , ui(new Ui::ConfigDialog) { mainWindow = parent; ui->setupUi(this); ui->profileStorePath->setText(Settings::getPassStore()); ui->currentPathLineEdit->setText(Settings::getPassStore()); ui->spinBoxAutoclearSeconds->setValue(Settings::getAutoclearSeconds()); ui->spinBoxAutoclearPanelSeconds->setValue(Settings::getAutoclearPanelSeconds()); ui->checkBoxHideContent->setChecked(Settings::isHideContent()); ui->checkBoxDisplayAsIs->setChecked(Settings::isDisplayAsIs()); ui->checkBoxNoLineWrapping->setChecked(Settings::isNoLineWrapping()); ui->plainTextEditTemplate->setPlainText(Settings::getPassTemplate()); ui->checkBoxTemplateAllFields->setChecked(Settings::isTemplateAllFields()); setProfiles(Settings::getProfiles(), Settings::getProfile()); setPasswordConfiguration(Settings::getPasswordConfiguration()); useAutoclear(Settings::isUseAutoclear()); useAutoclearPanel(Settings::isUseAutoclearPanel()); validateNewProfile(); useTemplate(Settings::isUseTemplate()); ui->profileTable->verticalHeader()->hide(); ui->profileTable->horizontalHeader()->setStretchLastSection(true); ui->label->setText(ui->label->text() + QString::fromUtf8(GPGPASS_VERSION_STRING)); // Generic setup connect(this, &ConfigDialog::accepted, this, &ConfigDialog::on_accepted); // General tab connect(ui->toolButtonStore, &QAbstractButton::clicked, this, &ConfigDialog::selectStoreFolder); connect(ui->checkBoxAutoclearPanel, &QAbstractButton::toggled, this, &ConfigDialog::toggleAutoClearPanelSubentries); // Profiles tab connect(ui->addButton, &QAbstractButton::clicked, this, &ConfigDialog::addProfile); connect(ui->deleteButton, &QAbstractButton::clicked, this, &ConfigDialog::deleteSelectedProfile); connect(ui->createNewProfileFolder, &QAbstractButton::toggled, this, &ConfigDialog::validateNewProfile); connect(ui->newProfileNameEdit, &QLineEdit::textChanged, this, &ConfigDialog::validateNewProfile); connect(ui->profileStorePath, &QLineEdit::textChanged, this, &ConfigDialog::validateNewProfile); connect(ui->profileTable, &QTableWidget::itemSelectionChanged, this, [this]() { ui->deleteButton->setEnabled(!ui->profileTable->selectedItems().isEmpty()); }); // template tab connect(ui->checkBoxUseTemplate, &QAbstractButton::toggled, this, &ConfigDialog::toggleTemplateSubentries); } bool ConfigDialog::checkNewProfileData() { QString name = ui->newProfileNameEdit->text(); if (name.isEmpty()) { return false; } QString path = ui->profileStorePath->text(); if (path.isEmpty()) { return false; } auto nameItem = ui->profileTable->findItems(name, Qt::MatchExactly); if (!nameItem.empty()) { return false; } auto pathItem = ui->profileTable->findItems(path, Qt::MatchExactly); if (!pathItem.empty()) { return false; } QFileInfo fi(path); if (fi.exists()) { return fi.isDir(); } else { return ui->createNewProfileFolder->isChecked(); } } void ConfigDialog::validateNewProfile() { ui->addButton->setEnabled(checkNewProfileData()); } ConfigDialog::~ConfigDialog() = default; void ConfigDialog::on_accepted() { Settings::setPassStore(Util::normalizeFolderPath(ui->currentPathLineEdit->text())); Settings::setUseAutoclear(ui->checkBoxAutoclear->isChecked()); Settings::setAutoclearSeconds(ui->spinBoxAutoclearSeconds->value()); Settings::setUseAutoclearPanel(ui->checkBoxAutoclearPanel->isChecked()); Settings::setAutoclearPanelSeconds(ui->spinBoxAutoclearPanelSeconds->value()); Settings::setHideContent(ui->checkBoxHideContent->isChecked()); Settings::setDisplayAsIs(ui->checkBoxDisplayAsIs->isChecked()); Settings::setNoLineWrapping(ui->checkBoxNoLineWrapping->isChecked()); Settings::setProfiles(getProfiles()); Settings::setPasswordConfiguration(getPasswordConfiguration()); Settings::setUseTemplate(ui->checkBoxUseTemplate->isChecked()); Settings::setPassTemplate(ui->plainTextEditTemplate->toPlainText()); Settings::setTemplateAllFields(ui->checkBoxTemplateAllFields->isChecked()); Settings::setVersion(QString::fromUtf8(GPGPASS_VERSION_STRING)); } /** * @brief ConfigDialog::selectFolder pop-up to choose a folder. * @return */ QString ConfigDialog::selectFolder() { QFileDialog dialog(this); dialog.setFileMode(QFileDialog::Directory); dialog.setFilter(QDir::NoFilter); dialog.setOption(QFileDialog::ShowDirsOnly); if (dialog.exec()) return dialog.selectedFiles().constFirst(); return QString(); } /** * @brief ConfigDialog::on_toolButtonStore_clicked get .password-store * location.s */ void ConfigDialog::selectStoreFolder() { QString store = selectFolder(); if (!store.isEmpty()) ui->profileStorePath->setText(store); } /** * @brief ConfigDialog::on_checkBoxAutoclearPanel_clicked enable and disable * options based on autoclear use. */ void ConfigDialog::toggleAutoClearPanelSubentries(bool state) { ui->spinBoxAutoclearPanelSeconds->setEnabled(state); ui->labelPanelSeconds->setEnabled(state); } /** * @brief ConfigDialog::useAutoclear set the clipboard autoclear use from * MainWindow. * @param useAutoclear */ void ConfigDialog::useAutoclear(bool useAutoclear) { ui->checkBoxAutoclear->setChecked(useAutoclear); } /** * @brief ConfigDialog::useAutoclearPanel set the panel autoclear use from * MainWindow. * @param useAutoclearPanel */ void ConfigDialog::useAutoclearPanel(bool useAutoclearPanel) { ui->checkBoxAutoclearPanel->setChecked(useAutoclearPanel); toggleAutoClearPanelSubentries(useAutoclearPanel); } static std::unique_ptr createTableWidgetItem(const QString &str) { auto item = std::make_unique(str); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); return item; } /** * @brief ConfigDialog::setProfiles set the profiles and chosen profile from * MainWindow. * @param profiles * @param profile */ void ConfigDialog::setProfiles(const QHash& profiles, const QString& profile) { ui->profileTable->clear(); ui->profileTable->setSortingEnabled(false); QHashIterator i(profiles); while (i.hasNext()) { i.next(); if (!i.value().isEmpty() && !i.key().isEmpty()) { ui->profileTable->insertRow(0); ui->profileTable->setItem(0, 0, createTableWidgetItem(i.key()).release()); ui->profileTable->setItem(0, 1, createTableWidgetItem(i.value()).release()); } } auto selected = ui->profileTable->findItems(profile, Qt::MatchExactly); if (!selected.empty()) { ui->profileTable->selectRow(selected.front()->row()); } ui->profileTable->setSortingEnabled(true); } /** * @brief ConfigDialog::getProfiles return profile list. * @return */ QHash ConfigDialog::getProfiles() { QHash profiles; // Check? for (int i = 0; i < ui->profileTable->rowCount(); ++i) { QTableWidgetItem *pathItem = ui->profileTable->item(i, 1); if (nullptr != pathItem) { QTableWidgetItem *item = ui->profileTable->item(i, 0); if (item == nullptr) { continue; } profiles.insert(item->text(), pathItem->text()); } } return profiles; } /** * @brief ConfigDialog::on_addButton_clicked add a profile row. */ void ConfigDialog::addProfile() { if (!checkNewProfileData()) { return; } ui->profileTable->setSortingEnabled(false); int n = ui->profileTable->rowCount(); ui->profileTable->insertRow(n); ui->profileTable->setItem(n, 0, createTableWidgetItem(ui->newProfileNameEdit->text()).release()); ui->profileTable->setItem(n, 1, createTableWidgetItem(ui->profileStorePath->text()).release()); ui->profileTable->selectRow(n); ui->deleteButton->setEnabled(true); ui->profileTable->setSortingEnabled(true); } /** * @brief ConfigDialog::on_deleteButton_clicked remove a profile row. */ void ConfigDialog::deleteSelectedProfile() { QSet selectedRows; // we use a set to prevent doubles const QList itemList = ui->profileTable->selectedItems(); if (itemList.count() == 0) { QMessageBox::warning(this, i18n("No profile selected"), i18n("No profile selected to delete")); return; } ui->profileTable->setSortingEnabled(false); for (const auto *item : itemList) selectedRows.insert(item->row()); // get a list, and sort it big to small QList rows = selectedRows.values(); std::sort(rows.begin(), rows.end()); // now actually do the removing: for (int row : qAsConst(rows)) ui->profileTable->removeRow(row); if (ui->profileTable->rowCount() < 1) ui->deleteButton->setEnabled(false); ui->profileTable->setSortingEnabled(true); } void ConfigDialog::setPasswordConfiguration(const PasswordConfiguration &config) { ui->spinBoxPasswordLength->setValue(config.length); ui->passwordCharTemplateSelector->setCurrentIndex(config.selected); } PasswordConfiguration ConfigDialog::getPasswordConfiguration() { PasswordConfiguration config; config.length = ui->spinBoxPasswordLength->value(); config.selected = static_cast(ui->passwordCharTemplateSelector->currentIndex()); return config; } /** * @brief ConfigDialog::on_checkBoxUseTemplate_clicked enable or disable the * template field and options. */ void ConfigDialog::toggleTemplateSubentries(bool enable) { ui->plainTextEditTemplate->setEnabled(enable); ui->checkBoxTemplateAllFields->setEnabled(enable); } /** * @brief ConfigDialog::useTemplate set preference for using templates. * @param useTemplate */ void ConfigDialog::useTemplate(bool useTemplate) { ui->checkBoxUseTemplate->setChecked(useTemplate); toggleTemplateSubentries(useTemplate); } diff --git a/src/firsttimedialog.cpp b/src/firsttimedialog.cpp index e337ea5..396769a 100644 --- a/src/firsttimedialog.cpp +++ b/src/firsttimedialog.cpp @@ -1,296 +1,296 @@ /* SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "firsttimedialog.h" #include "qdebug.h" #include #include #include #include #include #include #include "gpgmehelpers.h" #include "settings.h" #include "ui_keygenwidget.h" #include "ui_selectpasswordstore.h" #include "ui_userswidget.h" #include "util.h" #include #include #include -#include +#include DialogState::DialogState(Pass &p) : pass(p) { } QList DialogState::privateKeys() const { auto job = protocol->keyListJob(); std::vector keys; auto result = job->exec(QStringList(), true, keys); if (!isSuccess(result.error())) { return {}; } QList users; for (const auto &key : keys) { UserInfo ui; - ui.created = QDateTime::fromTime_t(key.subkey(0).creationTime()); + ui.created = QDateTime::fromSecsSinceEpoch(key.subkey(0).creationTime()); ui.key_id = fromGpgmeCharStar(key.keyID()); ui.name = createCombinedNameString(key.userID(0)); ui.validity = key.userID(0).validityAsString(); - ui.expiry = QDateTime::fromTime_t(key.subkey(0).expirationTime()); + ui.expiry = QDateTime::fromSecsSinceEpoch(key.subkey(0).expirationTime()); ui.have_secret = key.hasSecret(); users.append(ui); } return users; } FirstTimeDialog::FirstTimeDialog(QWidget *mainWindow, Pass &pass) : m_mainWindow(mainWindow) , m_state(pass) { setWindowTitle(i18n("GnuPG Password Manager setup")); setPage(Intro, new IntroPage(m_state)); setPage(KeyGen, new KeyGenPage(m_state)); setPage(PasswordStore, new PasswordStorePage(m_state)); setPage(KeySelect, new KeySelectPage(m_state)); setPage(Done, new DonePage()); QTransform rotate; rotate.rotate(-90); setPixmap(QWizard::WatermarkPixmap, QPixmap(QLatin1String(":/artwork/gnupg-logo-320x100tr.png")).transformed(rotate)); setPixmap(QWizard::LogoPixmap, QPixmap(QLatin1String(":/artwork/64-gpgpass.png"))); setStartId(Intro); } void FirstTimeDialog::done(int i) { if (i == QDialog::DialogCode::Accepted) { QSettings s; s.setValue(QStringLiteral("setup"), true); if (Settings::getAutoclearSeconds() < 5) Settings::setAutoclearSeconds(10); if (Settings::getAutoclearPanelSeconds() < 5) Settings::setAutoclearPanelSeconds(10); Settings::setPassTemplate(QStringLiteral("login\nurl")); m_mainWindow->show(); } QWizard::done(i); } int FirstTimeDialog::nextId() const { switch (currentId()) { case Intro: if (m_state.privateKeys().isEmpty()) { return KeyGen; } else { return PasswordStore; } case KeyGen: return PasswordStore; case PasswordStore: if (QFile::exists(m_state.storePath + QStringLiteral("/.gpg-id"))) { return Done; } else { return KeySelect; } case KeySelect: return Done; default: return -1; } return -1; }; IntroPage::IntroPage(DialogState &s) : m_state(s) { QVBoxLayout *lay = new QVBoxLayout(); lay->addWidget(new QLabel(i18n("Welcome to GnuPG Password manager"))); setTitle(i18n("Welcome")); setSubTitle(i18n("Setting up")); setLayout(lay); } KeyGenPage::KeyGenPage(DialogState &s) : m_state(s) { setTitle(i18n("Generate keys")); setSubTitle(i18n("Generate keys")); m_ui = std::make_unique(); m_ui->setupUi(this); m_ui->spinner->hide(); m_ui->generateButton->setEnabled(false); m_ui->message->hide(); m_ui->email->setValidator( new QRegularExpressionValidator(QRegularExpression(QRegularExpression::anchoredPattern(QStringLiteral(R"(\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b)")), QRegularExpression::CaseInsensitiveOption))); connect(m_ui->generateButton, &QPushButton::clicked, this, &KeyGenPage::startKeyGen); connect(m_ui->email, &QLineEdit::textChanged, this, &KeyGenPage::checkEntries); connect(m_ui->name, &QLineEdit::textChanged, this, &KeyGenPage::checkEntries); } void KeyGenPage::startKeyGen() { m_ui->email->setEnabled(false); m_ui->name->setEnabled(false); m_ui->generateButton->setEnabled(false); m_ui->includeComment->setEnabled(false); m_ui->spinner->show(); auto job = m_state.protocol->quickJob(); connect(job, &QGpgME::QuickJob::result, this, [this](auto &&result, const QString &log, auto &&logResult) { if (isSuccess(result)) { Q_EMIT this->completeChanged(); this->m_ui->spinner->hide(); this->m_ui->message->show(); this->m_ui->message->setText(i18n("Key generated")); } else { this->m_ui->message->setText(fromGpgmeCharStar(result.asString())); this->m_ui->message->show(); } Q_UNUSED(log); Q_UNUSED(logResult); }); QString uid; if (m_ui->includeComment->isChecked()) { uid = QStringLiteral("%1 (PasswordManager) <%2>"); } else { uid = QStringLiteral("%1 <%2>"); } job->startCreate(uid.arg(m_ui->name->text(), m_ui->email->text()), "future-default"); } void KeyGenPage::checkEntries() { auto emailValidator = m_ui->email->validator(); bool enable = false; if (emailValidator) { auto email = m_ui->email->text(); int i = 0; if (emailValidator->validate(email, i) == QValidator::Acceptable) { qDebug() << "email valid"; enable = m_ui->name->text().size() > 4; } else { qDebug() << "email invalid"; } } else { qDebug() << "no email validator"; // TODO_REMOV enable = true; } m_ui->generateButton->setEnabled(enable); } KeyGenPage::~KeyGenPage() = default; bool KeyGenPage::isComplete() const { return !m_state.privateKeys().isEmpty(); } PasswordStorePage::PasswordStorePage(DialogState &s) : m_ui(std::make_unique()) , m_state(s) { setTitle(i18n("Select password store")); setSubTitle(i18n("Folder for selection")); m_ui->setupUi(this); m_ui->lineEdit->setText(Util::findPasswordStore()); connect(m_ui->pushButton, &QPushButton::clicked, this, [this]() { QFileDialog dialog(this); dialog.setFileMode(QFileDialog::Directory); dialog.setFilter(QDir::NoFilter); dialog.setOption(QFileDialog::ShowDirsOnly); QString result; if (dialog.exec()) result = dialog.selectedFiles().constFirst(); m_ui->lineEdit->setText(result); }); m_ui->label->hide(); connect(m_ui->lineEdit, &QLineEdit::textChanged, this, &PasswordStorePage::completeChanged); connect(m_ui->checkBox, &QCheckBox::stateChanged, this, &PasswordStorePage::completeChanged); } bool PasswordStorePage::isComplete() const { QString dir = m_ui->lineEdit->text(); if (QDir(dir).exists()) { m_ui->label->hide(); return true; } if (m_ui->checkBox->isChecked()) { m_ui->label->hide(); return true; } m_ui->label->show(); return false; } bool PasswordStorePage::validatePage() { m_state.storePath = Util::normalizeFolderPath(QDir::fromNativeSeparators(m_ui->lineEdit->text())); Settings::setPassStore(m_state.storePath); return QDir().mkpath(m_state.storePath); } PasswordStorePage::~PasswordStorePage() = default; KeySelectPage::KeySelectPage(DialogState &s) : m_ui(std::make_unique()) , m_state(s) , d(m_state.pass) { setTitle(i18n("KeySelectPage")); setSubTitle(i18n("Describe users with access")); m_ui->setupUi(this); d.setUi(m_ui.get()); connect(m_ui->listWidget, &QListWidget::itemChanged, this, &KeySelectPage::completeChanged); } void KeySelectPage::initializePage() { d.m_dir = m_state.storePath; d.generateUserList(); d.populateList(); } bool KeySelectPage::isComplete() const { for (const auto &item : std::as_const(d.m_userList)) { if (item.enabled) { return true; } } return false; } bool KeySelectPage::validatePage() { d.init(); return true; } KeySelectPage::~KeySelectPage() = default; DonePage::DonePage() { setTitle(i18n("Setup done")); setSubTitle(i18n("Thanks")); setFinalPage(true); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 255ee37..48a4186 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,988 +1,988 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2016-2017 tezeb SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "mainwindow.h" #include #include "clipboardhelper.h" #include "configdialog.h" #include "filecontent.h" #include "pass.h" #include "passworddialog.h" #include "qpushbuttonfactory.h" #include "settings.h" #include "ui_mainwindow.h" #include "usersdialog.h" #include "util.h" -#include -#include -#include +#include +#include +#include #include #include #include #include #include #include #include #include #include #include static QString directoryName(const QString &dirOrFile) { QFileInfo fi{dirOrFile}; if (fi.isDir()) { return fi.absoluteFilePath(); } else { return fi.absolutePath(); } } /** * @brief MainWindow::MainWindow handles all of the main functionality and also * the main window. * @param searchText for searching from cli * @param parent pointer */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , m_pass{std::make_unique()} , ui(new Ui::MainWindow) , proxyModel{*m_pass} { #ifdef __APPLE__ // extra treatment for mac os // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic qt_set_sequence_auto_mnemonic(true); #endif ui->setupUi(this); m_clipboardHelper = new ClipboardHelper(this); m_errorMessage = new KMessageWidget(); m_errorMessage->setMessageType(KMessageWidget::Error); m_errorMessage->hide(); ui->messagesArea->addWidget(m_errorMessage); connect(m_pass.get(), &Pass::errorString, this, [this](auto str) { m_errorMessage->setText(str); m_errorMessage->animatedShow(); setUiElementsEnabled(true); }); connect(m_pass.get(), &Pass::critical, this, &MainWindow::critical); connect(m_pass.get(), &Pass::finishedShow, this, &MainWindow::passShowHandler); connect(m_pass.get(), &Pass::finishedInsert, this, [this]() { this->selectTreeItem(this->getCurrentTreeViewIndex()); }); connect(m_pass.get(), &Pass::startReencryptPath, this, &MainWindow::startReencryptPath); connect(m_pass.get(), &Pass::endReencryptPath, this, &MainWindow::endReencryptPath); { m_notInitialized = new KMessageWidget(i18n("Password store not initialized")); m_notInitialized->setCloseButtonVisible(false); QAction* action = new QAction(i18n("Initialize with users")); connect(action, &QAction::triggered, this, [this](){this->userDialog();}); m_notInitialized->addAction(action); m_notInitialized->setMessageType(KMessageWidget::Error); ui->messagesArea->addWidget(m_notInitialized); m_notInitialized->hide(); } { auto w = new QWidget(); w->setLayout(new QHBoxLayout); auto l = new QLabel(i18n("Select profile")); auto sep = ui->toolBar->addSeparator(); w->layout()->addWidget(l); m_profileBox = new QComboBox(); w->layout()->addWidget(m_profileBox); m_profiles = ui->toolBar->addWidget(w); connect(m_profiles, &QAction::changed, sep, [this, sep]() { sep->setVisible(this->m_profiles->isVisible()); }); } } void MainWindow::setVisible(bool visible) { // This originated in the ctor, but we want this to happen after the first start wizard has been run // so for now, moved to show() on first call if (visible && firstShow) { firstShow = false; // register shortcut ctrl/cmd + Q to close the main window new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close())); model.setNameFilters(QStringList() << QStringLiteral("*.gpg")); model.setNameFilterDisables(false); /* * I added this to solve Windows bug but now on GNU/Linux the main folder, * if hidden, disappear * * model.setFilter(QDir::NoDot); */ QString passStore = Settings::getPassStore(Util::findPasswordStore()); QModelIndex rootDir = model.setRootPath(passStore); model.fetchMore(rootDir); proxyModel.setSourceModel(&model); selectionModel.reset(new QItemSelectionModel(&proxyModel)); ui->treeView->setModel(&proxyModel); ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir)); ui->treeView->setColumnHidden(1, true); ui->treeView->setColumnHidden(2, true); ui->treeView->setColumnHidden(3, true); ui->treeView->setHeaderHidden(true); ui->treeView->setIndentation(15); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->treeView->sortByColumn(0, Qt::AscendingOrder); connect(ui->treeView, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenu); connect(ui->treeView, &DeselectableTreeView::emptyClicked, this, &MainWindow::deselect); if (Settings::isNoLineWrapping()) { ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap); } ui->textBrowser->setOpenExternalLinks(true); ui->textBrowser->setContextMenuPolicy(Qt::DefaultContextMenu); updateProfileBox(); clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds()); clearPanelTimer.setSingleShot(true); connect(&clearPanelTimer, SIGNAL(timeout()), this, SLOT(clearPanel())); searchTimer.setInterval(350); searchTimer.setSingleShot(true); connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch); initToolBarButtons(); initStatusBar(); ui->lineEdit->setClearButtonEnabled(true); setUiElementsEnabled(true); QTimer::singleShot(10, this, SLOT(focusInput())); verifyInitialized(); } QMainWindow::setVisible(visible); } MainWindow::~MainWindow() = default; /** * @brief MainWindow::focusInput selects any text (if applicable) in the search * box and sets focus to it. Allows for easy searching, called at application * start and when receiving empty message in MainWindow::messageAvailable when * compiled with SINGLE_APP=1 (default). */ void MainWindow::focusInput() { ui->lineEdit->selectAll(); ui->lineEdit->setFocus(); } /** * @brief MainWindow::changeEvent sets focus to the search box * @param event */ void MainWindow::changeEvent(QEvent *event) { QWidget::changeEvent(event); if (event->type() == QEvent::ActivationChange) { if (isActiveWindow()) { focusInput(); } } } /** * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions */ void MainWindow::initToolBarButtons() { connect(ui->actionAddPassword, &QAction::triggered, this, &MainWindow::addPassword); connect(ui->actionAddFolder, &QAction::triggered, this, &MainWindow::addFolder); connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit); connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete); connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers); connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig); connect(ui->treeView, &QTreeView::clicked, this, &MainWindow::selectTreeItem); connect(ui->treeView, &QTreeView::doubleClicked, this, &MainWindow::editTreeItem); connect(m_profileBox, &QComboBox::currentTextChanged, this, &MainWindow::selectProfile); connect(ui->lineEdit, &QLineEdit::textChanged, this, &MainWindow::filterList); connect(ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::selectFromSearch); ui->actionAddPassword->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); ui->actionAddFolder->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); ui->actionEdit->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); ui->actionDelete->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); ui->actionUsers->setIcon(QIcon::fromTheme(QStringLiteral("x-office-address-book"))); ui->actionConfig->setIcon(QIcon::fromTheme(QStringLiteral("applications-system"))); } /** * @brief MainWindow::initStatusBar init statusBar with default message and logo */ void MainWindow::initStatusBar() { ui->statusBar->showMessage(i18nc("placeholder is version number","Welcome to GnuPG Password Manager %1", QString::fromLocal8Bit(GPGPASS_VERSION_STRING))); QPixmap logo = QPixmap(QStringLiteral(":/artwork/32-gpgpass.png")); QLabel *logoApp = new QLabel(statusBar()); logoApp->setPixmap(logo); statusBar()->addPermanentWidget(logoApp); } const QModelIndex MainWindow::getCurrentTreeViewIndex() { return ui->treeView->currentIndex(); } void MainWindow::showRemainingHtml(const QString &text) { QString _text = text; if (!ui->textBrowser->toPlainText().isEmpty()) _text = ui->textBrowser->toHtml() + _text; ui->textBrowser->setHtml(_text); } /** * @brief MainWindow::config pops up the configuration screen and handles all * inter-window communication */ void MainWindow::config() { QScopedPointer d(new ConfigDialog(this)); d->setModal(true); if (d->exec()) { if (d->result() == QDialog::Accepted) { // Update the textBrowser line wrap mode if (Settings::isNoLineWrapping()) { ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap); } else { ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth); } this->show(); updateProfileBox(); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); clearPanelTimer.setInterval(1000 * Settings::getAutoclearPanelSeconds()); m_clipboardHelper->setClipboardTimer(); } } } /** * @brief MainWindow::on_treeView_clicked read the selected password file * @param index */ void MainWindow::selectTreeItem(const QModelIndex &index) { bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags; // TODO(bezet): "Could not decrypt"; m_clipboardHelper->clearClippedText(); QString file = index.data(QFileSystemModel::FilePathRole).toString(); ui->passwordName->setText(index.data().toString()); if (!file.isEmpty() && QFileInfo(file).isFile() && !cleared) { m_pass->Show(file); } else { clearPanel(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(true); } } /** * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on * TreeViewItem, open the edit Window * @param index */ void MainWindow::editTreeItem(const QModelIndex &index) { QFileInfo fileOrFolder{index.data(QFileSystemModel::Roles::FilePathRole).toString()}; if (fileOrFolder.isFile()) { editPassword(fileOrFolder.absoluteFilePath()); } } /** * @brief MainWindow::deselect clear the selection, password and copy buffer */ void MainWindow::deselect() { m_clipboardHelper->clearClipboard(); ui->treeView->clearSelection(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); ui->passwordName->setText(QString{}); clearPanel(); } void MainWindow::passShowHandler(const QString &p_output) { QStringList templ = Settings::isUseTemplate() ? Settings::getPassTemplate().split(QStringLiteral("\n")) : QStringList(); bool allFields = Settings::isUseTemplate() && Settings::isTemplateAllFields(); FileContent fileContent = FileContent::parse(p_output, templ, allFields); QString output = p_output; QString password = fileContent.getPassword(); // set clipped text m_clipboardHelper->setClippedText(password); // first clear the current view: clearPanel(); // show what is needed: if (Settings::isHideContent()) { output = i18n("*** Content hidden ***"); } else if (!Settings::isDisplayAsIs()) { if (!password.isEmpty()) { // set the password, it is hidden if needed in addToGridLayout addToGridLayout(i18n("Password"), password); } const NamedValues namedValues = fileContent.getNamedValues(); for (const auto& nv : namedValues) { addToGridLayout(nv.name, nv.value); } output = fileContent.getRemainingDataForDisplay(); } if (Settings::isUseAutoclearPanel()) { clearPanelTimer.start(); } output = output.toHtmlEscaped(); output.replace(Util::protocolRegex(), QStringLiteral(R"(\1)")); output.replace(QStringLiteral("\n"), QStringLiteral("
")); showRemainingHtml(output); setUiElementsEnabled(true); m_errorMessage->animatedHide(); } /** * @brief MainWindow::clearPanel hide the information from shoulder surfers */ void MainWindow::clearPanel() { clearTemplateWidgets(); ui->textBrowser->setHtml(QString{}); } /** * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI * elements * @param state */ void MainWindow::setUiElementsEnabled(bool state) { ui->treeView->setEnabled(state); ui->lineEdit->setEnabled(state); ui->lineEdit->installEventFilter(this); ui->actionAddPassword->setEnabled(state); ui->actionAddFolder->setEnabled(state); ui->actionUsers->setEnabled(state); ui->actionConfig->setEnabled(state); // is a file selected? state &= ui->treeView->currentIndex().isValid(); ui->actionDelete->setEnabled(state); ui->actionEdit->setEnabled(state); } /** * @brief MainWindow::on_configButton_clicked run Mainwindow::config */ void MainWindow::onConfig() { config(); } /** * @brief Executes when the string in the search box changes, collapses the * TreeView * @param arg1 */ void MainWindow::filterList(const QString &arg1) { ui->statusBar->showMessage(i18n("Looking for: %1", arg1), 1000); ui->treeView->expandAll(); clearPanel(); ui->passwordName->setText(QString{}); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); searchTimer.start(); } /** * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much * time from two keypresses is elapsed */ void MainWindow::onTimeoutSearch() { QString query = ui->lineEdit->text(); if (query.isEmpty()) { ui->treeView->collapseAll(); deselect(); } query.replace(QStringLiteral(" "), QStringLiteral(".*")); QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption); proxyModel.setFilterRegularExpression(regExp); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); if (proxyModel.rowCount() > 0 && !query.isEmpty()) { selectFirstFile(); } else { ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); } } /** * @brief MainWindow::on_lineEdit_returnPressed get searching * * Select the first possible file in the tree */ void MainWindow::selectFromSearch() { if (proxyModel.rowCount() > 0) { selectFirstFile(); selectTreeItem(ui->treeView->currentIndex()); } } /** * @brief MainWindow::selectFirstFile select the first possible file in the * tree */ void MainWindow::selectFirstFile() { QModelIndex index = proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore())); index = firstFile(index); ui->treeView->setCurrentIndex(index); } /** * @brief MainWindow::firstFile return location of first possible file * @param parentIndex * @return QModelIndex */ QModelIndex MainWindow::firstFile(QModelIndex parentIndex) { QModelIndex index = parentIndex; int numRows = proxyModel.rowCount(parentIndex); for (int row = 0; row < numRows; ++row) { index = proxyModel.index(row, 0, parentIndex); if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) return index; if (proxyModel.hasChildren(index)) return firstFile(index); } return index; } /** * @brief MainWindow::setPassword open passworddialog * @param file which pgp file * @param isNew insert (not update) */ void MainWindow::setPassword(QString file, bool isNew) { PasswordDialog d(*m_pass, file, isNew, this); if (!d.exec()) { ui->treeView->setFocus(); } } /** * @brief MainWindow::addPassword add a new password by showing a * number of dialogs. */ void MainWindow::addPassword() { bool ok; QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString()); if (dir.isEmpty()) { dir = Settings::getPassStore(); } QString file = QInputDialog::getText(this, i18n("New file"), i18n("New password file: \n(Will be placed in %1 )", dir), QLineEdit::Normal, QString{}, &ok); if (!ok || file.isEmpty()) return; file = QDir(dir).absoluteFilePath(file + QStringLiteral(".gpg")); setPassword(file); } /** * @brief MainWindow::onDelete remove password, if you are * sure. */ void MainWindow::onDelete() { QModelIndex currentIndex = ui->treeView->currentIndex(); if (!currentIndex.isValid()) { // If not valid, we might end up passing empty string // to delete, and taht might delete unexpected things on disk return; } - QFileInfo fileOrFolder = currentIndex.data(QFileSystemModel::FilePathRole).toString(); + QFileInfo fileOrFolder{currentIndex.data(QFileSystemModel::FilePathRole).toString()}; bool isDir = fileOrFolder.isDir(); QString file = fileOrFolder.absoluteFilePath(); QString message; if (isDir) { message = i18nc("deleting a folder; placeholder is folder name","Are you sure you want to delete %1 and the whole content?", file); QDirIterator it(model.rootPath() + QLatin1Char('/') + file, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); if (auto fi = it.fileInfo(); fi.isFile()) { if (fi.suffix() != QStringLiteral("gpg")) { message += QStringLiteral("
") + i18nc("extra warning during certain folder deletions","Attention: " "there are unexpected files in the given folder, " "check them before continue") + QStringLiteral(""); break; } } } } else { message = i18nc("deleting a file; placeholder is file name","Are you sure you want to delete %1?", file); } if (QMessageBox::question(this, isDir ? i18n("Delete folder?") : i18n("Delete password?"), message, QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) return; m_pass->Remove(file, isDir); } /** * @brief MainWindow::onEdit try and edit (selected) password. */ void MainWindow::onEdit() { QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString(); if (!file.isEmpty()) { editPassword(file); } } /** * @brief MainWindow::userDialog see MainWindow::onUsers() * @param dir folder to edit users for. */ void MainWindow::userDialog(QString dir) { if (dir.isEmpty()) { dir = Settings::getPassStore(); } QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } UsersDialog d(dir, *m_pass, this); if (!d.exec()) { ui->treeView->setFocus(); } verifyInitialized(); } /** * @brief MainWindow::onUsers edit users for the current * folder, * gets lists and opens UserDialog. */ void MainWindow::onUsers() { QString dir = ui->treeView->currentIndex().data(QFileSystemModel::Roles::FilePathRole).toString(); if (dir.isEmpty()) { dir = Settings::getPassStore(); } else { QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } dir = Util::normalizeFolderPath(dir); } userDialog(dir); } /** * @brief MainWindow::updateProfileBox update the list of profiles, optionally * select a more appropriate one to view too */ void MainWindow::updateProfileBox() { QHash profiles = Settings::getProfiles(); if (profiles.isEmpty()) { m_profiles->setVisible(false); } else { m_profiles->setVisible(true); m_profileBox->setEnabled(profiles.size() > 1); m_profileBox->clear(); QHashIterator i(profiles); while (i.hasNext()) { i.next(); if (!i.key().isEmpty()) m_profileBox->addItem(i.key()); } m_profileBox->model()->sort(0); } int index = m_profileBox->findText(Settings::getProfile()); if (index != -1) // -1 for not found m_profileBox->setCurrentIndex(index); } /** * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the * correct "profile" * @param name */ void MainWindow::selectProfile(QString name) { if (name == Settings::getProfile()) return; ui->lineEdit->clear(); clearPanel(); Settings::setProfile(name); Settings::setPassStore(Settings::getProfiles().value(name)); ui->statusBar->showMessage(i18n("Profile changed to %1", name), 2000); ui->treeView->selectionModel()->clear(); ui->treeView->setRootIndex(proxyModel.mapFromSource(model.setRootPath(Settings::getPassStore()))); verifyInitialized(); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); } void MainWindow::verifyInitialized() { bool actionsEnabled; if (!QFile::exists(Settings::getPassStore() + QStringLiteral("/.gpg-id"))) { m_notInitialized->animatedShow(); actionsEnabled = false; } else { m_notInitialized->animatedHide(); actionsEnabled = true; } ui->actionAddFolder->setEnabled(actionsEnabled); ui->actionAddPassword->setEnabled(actionsEnabled); ui->actionDelete->setEnabled(ui->actionDelete->isEnabled() && actionsEnabled); ui->actionEdit->setEnabled(ui->actionEdit->isEnabled() && actionsEnabled); } /** * @brief MainWindow::closeEvent hide or quit * @param event */ void MainWindow::closeEvent(QCloseEvent *event) { m_clipboardHelper->clearClipboard(); event->accept(); } /** * @brief MainWindow::eventFilter filter out some events and focus the * treeview * @param obj * @param event * @return */ bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) { auto *key = dynamic_cast(event); if (key != nullptr && key->key() == Qt::Key_Down) { ui->treeView->setFocus(); } } return QObject::eventFilter(obj, event); } /** * @brief MainWindow::keyPressEvent did anyone press return, enter or escape? * @param event */ void MainWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Delete: onDelete(); break; case Qt::Key_Return: case Qt::Key_Enter: if (proxyModel.rowCount() > 0) selectTreeItem(ui->treeView->currentIndex()); break; case Qt::Key_Escape: ui->lineEdit->clear(); break; default: break; } } /** * @brief MainWindow::showContextMenu show us the (file or folder) context * menu * @param pos */ void MainWindow::showContextMenu(const QPoint &pos) { QModelIndex index = ui->treeView->indexAt(pos); bool selected = true; if (!index.isValid()) { ui->treeView->clearSelection(); ui->actionDelete->setEnabled(false); ui->actionEdit->setEnabled(false); selected = false; } ui->treeView->setCurrentIndex(index); QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos); QFileInfo fileOrFolder = model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex())); QMenu contextMenu; if (!selected || fileOrFolder.isDir()) { const QAction *addFolderAction = contextMenu.addAction(i18n("Add folder")); const QAction *addPasswordAction = contextMenu.addAction(i18n("Add password")); const QAction *usersAction = contextMenu.addAction(i18n("Users")); connect(addFolderAction, &QAction::triggered, this, &MainWindow::addFolder); connect(addPasswordAction, &QAction::triggered, this, &MainWindow::addPassword); connect(usersAction, &QAction::triggered, this, &MainWindow::onUsers); } else if (fileOrFolder.isFile()) { const QAction *edit = contextMenu.addAction(i18n("Edit")); connect(edit, &QAction::triggered, this, &MainWindow::onEdit); } if (selected) { contextMenu.addSeparator(); if (fileOrFolder.isDir()) { const QAction *renameFolderAction = contextMenu.addAction(i18n("Rename folder")); connect(renameFolderAction, &QAction::triggered, this, &MainWindow::renameFolder); } else if (fileOrFolder.isFile()) { const QAction *renamePasswordAction = contextMenu.addAction(i18n("Rename password")); connect(renamePasswordAction, &QAction::triggered, this, &MainWindow::renamePassword); } const QAction *deleteItem = contextMenu.addAction(i18n("Delete")); connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete); } contextMenu.exec(globalPos); } /** * @brief MainWindow::addFolder add a new folder to store passwords in */ void MainWindow::addFolder() { bool ok; QString dir = directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString()); if (dir.isEmpty()) { dir = Settings::getPassStore(); } QString newdir = QInputDialog::getText(this, i18n("New file"), i18n("New Folder: \n(Will be placed in %1 )", dir), QLineEdit::Normal, QString{}, &ok); if (!ok || newdir.isEmpty()) return; QDir(dir).mkdir(newdir); } /** * @brief MainWindow::renameFolder rename an existing folder */ void MainWindow::renameFolder() { bool ok; QString srcDir = QDir::cleanPath(directoryName(ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString())); if (srcDir.isEmpty()) { return; } QString srcDirName = QDir(srcDir).dirName(); QString newName = QInputDialog::getText(this, i18n("Rename file"), i18n("Rename Folder To: "), QLineEdit::Normal, srcDirName, &ok); if (!ok || newName.isEmpty()) return; QString destDir = srcDir; destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName); m_pass->Move(srcDir, destDir); } /** * @brief MainWindow::editPassword read password and open edit window via * MainWindow::onEdit() */ void MainWindow::editPassword(const QString &file) { if (!file.isEmpty()) { setPassword(file, false); } } /** * @brief MainWindow::renamePassword rename an existing password */ void MainWindow::renamePassword() { bool ok; QString file = ui->treeView->currentIndex().data(QFileSystemModel::FilePathRole).toString(); QString filePath = QFileInfo(file).path(); QString fileName = QFileInfo(file).fileName(); if (fileName.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive)) fileName.chop(4); QString newName = QInputDialog::getText(this, i18n("Rename file"), i18n("Rename File To: "), QLineEdit::Normal, fileName, &ok); if (!ok || newName.isEmpty()) return; QString newFile = QDir(filePath).filePath(newName); m_pass->Move(file, newFile); } /** * @brief MainWindow::clearTemplateWidgets empty the template widget fields in * the UI */ void MainWindow::clearTemplateWidgets() { while (ui->contentLayout->count() > 0) { ui->contentLayout->removeRow(ui->contentLayout->rowCount() -1); } } /** * @brief MainWindow::addToGridLayout add a field to the template grid * @param position * @param field * @param value */ void MainWindow::addToGridLayout(const QString &field, const QString &value) { QString trimmedField = field.trimmed(); QString trimmedValue = value.trimmed(); // Combine the Copy button and the line edit in one widget QFrame *frame = new QFrame(); QLayout *ly = new QHBoxLayout(); ly->setContentsMargins(5, 2, 2, 2); ly->setSpacing(0); frame->setLayout(ly); auto fieldLabel = createPushButton(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy '%1' to clipboard", trimmedField), m_clipboardHelper, [this, trimmedValue] { m_clipboardHelper->copyTextToClipboard(trimmedValue); }); frame->layout()->addWidget(fieldLabel.release()); auto qrButton = createPushButton(QIcon::fromTheme(QStringLiteral("view-barcode-qr")), i18n("View '%1' QR Code", trimmedField), m_clipboardHelper, [this, trimmedValue]() { - auto barcode = Prison::createBarcode(Prison::QRCode); + auto barcode = Prison::Barcode::create(Prison::QRCode); if (!barcode) { return; } barcode->setData(trimmedValue); auto image = barcode->toImage(barcode->preferredSize(window()->devicePixelRatioF())); QDialog popup(nullptr, Qt::Popup | Qt::FramelessWindowHint); QVBoxLayout *layout = new QVBoxLayout; QLabel *popupLabel = new QLabel(); layout->addWidget(popupLabel); popupLabel->setPixmap(QPixmap::fromImage(image)); popupLabel->setScaledContents(true); popupLabel->show(); popup.setLayout(layout); popup.move(QCursor::pos()); popup.exec(); }); frame->layout()->addWidget(qrButton.release()); if (trimmedField == i18n("Password")) { auto *line = new QLineEdit(); line->setObjectName(trimmedField); line->setText(trimmedValue); line->setReadOnly(true); line->setContentsMargins(0, 0, 0, 0); line->setEchoMode(QLineEdit::Password); auto icon = QIcon::fromTheme(QStringLiteral("password-show-on")); icon.addFile(QStringLiteral("password-show-off"), QSize(), QIcon::Normal, QIcon::Off); auto showButton = createPushButton(icon, i18n("Toggle password visibility"), line, [line]() { if (line->echoMode() == QLineEdit::Password) { line->setEchoMode(QLineEdit::Normal); } else { line->setEchoMode(QLineEdit::Password); } }); showButton->setCheckable(true); showButton->setContentsMargins(0, 0, 0, 0); frame->layout()->addWidget(showButton.release()); frame->layout()->addWidget(line); } else { auto *line = new QLabel(); line->setOpenExternalLinks(true); line->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); line->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum)); line->setObjectName(trimmedField); trimmedValue.replace(Util::protocolRegex(), QStringLiteral(R"(\1)")); line->setText(trimmedValue); line->setContentsMargins(5, 0, 0, 0); frame->layout()->addWidget(line); } // set into the layout ui->contentLayout->addRow(trimmedField, frame); } /** * @brief MainWindow::startReencryptPath disable ui elements and treeview */ void MainWindow::startReencryptPath() { statusBar()->showMessage(i18n("Re-encrypting folders"), 3000); setUiElementsEnabled(false); ui->treeView->setDisabled(true); } /** * @brief MainWindow::endReencryptPath re-enable ui elements */ void MainWindow::endReencryptPath() { setUiElementsEnabled(true); } /** * @brief MainWindow::critical critical message popup wrapper. * @param title * @param msg */ void MainWindow::critical(const QString& title, const QString& msg) { QMessageBox::critical(this, title, msg); } diff --git a/src/pass.cpp b/src/pass.cpp index 86bc5d6..0393128 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -1,437 +1,437 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2017 Jason A. Donenfeld SPDX-FileCopyrightText: 2020 Charlie Waters SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "pass.h" #include "gpgmehelpers.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -#include +#include // TODO remove #include using namespace std; // TODO remove Q_REQUIRED_RESULT static bool ensurePassStore(const QString &file, const char *location) { if (file.startsWith(Settings::getPassStore())) { return true; } QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file not in pass store called from %1 %2 %3").arg(QString::fromLocal8Bit(location),file,Settings::getPassStore())); return false; } // TODO remove Q_REQUIRED_RESULT static bool ensureSuffix(const QString &file, const char *location) { if (file.endsWith(QStringLiteral(".gpg"))) { return true; } qWarning() << "file without suffix called from " << location; QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file without suffix called from %1 %2").arg(QString::fromLocal8Bit(location),file)); return false; } /** * @brief Pass::Pass wrapper for using either pass or the pass imitation */ Pass::Pass() { } /** * @brief Pass::Generate use either pwgen or internal password * generator * @param length of the desired password * @param charset to use for generation * @return the password */ QString Pass::Generate_b(unsigned int length, const QString &charset) { if (charset.length() > 0) { return generateRandomPassword(charset, length); } else { Q_EMIT critical(i18n("No characters chosen"), i18n("Can't generate password, there are no characters to choose from " "set in the configuration!")); } return {}; } /** * @brief Pass::listKeys list users * @param keystrings * @param secret list private keys * @return QList users */ QList Pass::listKeys(const QStringList& keystrings, bool secret) { auto job = protocol->keyListJob(); std::vector keys; job->addMode(GpgME::KeyListMode::WithSecret); auto result = job->exec(keystrings, secret, keys); if (!isSuccess(result.error())) { return {}; } QList users; for (const auto &key : keys) { UserInfo ui; - ui.created = QDateTime::fromTime_t(key.subkey(0).creationTime()); + ui.created = QDateTime::fromSecsSinceEpoch(key.subkey(0).creationTime()); ui.key_id = fromGpgmeCharStar(key.keyID()); ui.name = createCombinedNameString(key.userID(0)); ui.validity = key.userID(0).validityAsString(); - ui.expiry = QDateTime::fromTime_t(key.subkey(0).expirationTime()); + ui.expiry = QDateTime::fromSecsSinceEpoch(key.subkey(0).expirationTime()); ui.have_secret = key.hasSecret(); users.append(ui); } return users; } /** * @brief Pass::listKeys list users * @param keystring * @param secret list private keys * @return QList users */ QList Pass::listKeys(const QString& keystring, bool secret) { return listKeys(QStringList(keystring), secret); } /** * @brief Pass::getRecipientList return list of gpg-id's to encrypt for * @param for_file which file (folder) would you like recepients for * @return recepients gpg-id contents */ QStringList Pass::getRecipientList(const QString& for_file) { if (!ensurePassStore(for_file, Q_FUNC_INFO)) { return {}; } QDir gpgIdPath(QFileInfo(for_file).absoluteDir()); bool found = false; while (gpgIdPath.exists() && gpgIdPath.absolutePath().startsWith(Settings::getPassStore())) { if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) { found = true; break; } if (!gpgIdPath.cdUp()) break; } QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : Settings::getPassStore() + QStringLiteral(".gpg-id")); if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) return QStringList(); QStringList recipients; while (!gpgId.atEnd()) { QString recipient(QString::fromLocal8Bit(gpgId.readLine())); recipient = recipient.trimmed(); if (!recipient.isEmpty()) recipients += recipient; } return recipients; } void Pass::Show(const QString& file) { if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) { return; } QFile f(file); f.open(QIODevice::ReadOnly); auto data = f.readAll(); auto job = protocol->decryptJob(); connect(job, &QGpgME::DecryptJob::result, this, [this](auto &&result, QByteArray plainText, QString auditLog, auto &&logError) { if (isSuccess(result.error())) { Q_EMIT finishedShow(QString::fromUtf8(plainText)); } else { Q_EMIT errorString(QString::fromLocal8Bit(result.error().asString())); Q_EMIT decryptionError(); } // Q_EMIT log(auditLog); Q_UNUSED(logError); Q_UNUSED(auditLog); }); job->start(data); } void Pass::Insert(const QString& file, const QString& newValue) { if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) { return; } QStringList recipients = Pass::getRecipientList(file); auto job = protocol->encryptJob(); std::vector keys; auto ctx = QGpgME::Job::context(job); for (const auto &keyId : recipients) { GpgME::Error error; auto key = ctx->key(keyId.toUtf8().data(), error, false); if (!error && !key.isNull()) { keys.push_back(key); } } if (keys.empty()) { Q_EMIT critical(i18n("Can not edit"), i18n("Could not read encryption key to use, .gpg-id " "file missing or invalid.")); return; } connect(job, &QGpgME::EncryptJob::result, this, [this, file](auto &&result, const QByteArray &ciphertext, const QString &log, auto &&auditResult) { if (isSuccess(result.error())) { QSaveFile f(file); bool open = f.open(QIODevice::WriteOnly | QIODevice::Truncate); if (!open) { Q_EMIT errorString(QStringLiteral("File open failed: %1").arg(f.errorString())); return; } f.write(ciphertext); if (f.error() == QFileDevice::NoError) { f.commit(); Q_EMIT finishedInsert(); } else { Q_EMIT errorString(QStringLiteral("File write failed: %1").arg(f.errorString())); } } else { Q_EMIT errorString(QString::fromUtf8(result.error().asString())); } Q_UNUSED(log); Q_UNUSED(auditResult); }); job->start(keys, newValue.toUtf8()); } void Pass::Remove(const QString& file, bool isDir) { if (!ensurePassStore(file, Q_FUNC_INFO)) { return; } if (!isDir) { if(!ensureSuffix(file, Q_FUNC_INFO)) { return; } QFile(file).remove(); } else { QDir dir(file); dir.removeRecursively(); } } void Pass::Init(const QString& path, const QList &users) { if (!ensurePassStore(path, Q_FUNC_INFO)) { return; } QString gpgIdFile = path + QStringLiteral(".gpg-id"); QFile gpgId(gpgIdFile); if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) { Q_EMIT critical(i18n("Cannot update"), i18n("Failed to open .gpg-id for writing.")); return; } bool secret_selected = false; for (const UserInfo &user : users) { if (user.enabled) { gpgId.write((user.key_id + QStringLiteral("\n")).toUtf8()); secret_selected |= user.have_secret; } } gpgId.close(); if (!secret_selected) { Q_EMIT critical(i18n("Check selected users!"), i18n("None of the selected keys have a secret key available.\n" "You will not be able to decrypt any newly added passwords!")); return; } reencryptPath(path); } /** * @brief ImitatePass::reencryptPath reencrypt all files under the chosen * directory * * This is stil quite experimental.. * @param dir */ void Pass::reencryptPath(const QString &dir) { if (!ensurePassStore(dir, Q_FUNC_INFO)) { return; } Q_EMIT startReencryptPath(); QDir currentDir; QDirIterator gpgFiles(dir, QStringList() << QStringLiteral("*.gpg"), QDir::Files, QDirIterator::Subdirectories); QStringList gpgId; while (gpgFiles.hasNext()) { QString fileName = gpgFiles.next(); if (gpgFiles.fileInfo().path() != currentDir.path()) { gpgId = getRecipientList(fileName); gpgId.sort(); } QByteArray plainText; QFile f(fileName); if (!f.open(QIODevice::ReadOnly)) { Q_EMIT errorString(QStringLiteral("Failed to open file: %1").arg(fileName)); continue; } QByteArray cipherText = f.readAll(); f.close(); auto decryptJob = protocol->decryptJob(); auto context = QGpgME::Job::context(decryptJob); auto decryptResult = decryptJob->exec(cipherText, plainText); if (!isSuccess(decryptResult.error())) { Q_EMIT errorString(fromGpgmeCharStar(decryptResult.error().asString())); continue; } auto actualRecipients = decryptResult.recipients(); QStringList actualIds; for (auto &recipient : actualRecipients) { GpgME::Error error; auto key = context->key(recipient.keyID(), error, false); if (!error) { actualIds.append(fromGpgmeCharStar(key.keyID())); } } actualIds.sort(); if (actualIds != gpgId) { auto encryptJob = protocol->encryptJob(); std::vector keys; auto ctx = QGpgME::Job::context(encryptJob); for (const auto &keyId : qAsConst(gpgId)) { GpgME::Error error; auto key = ctx->key(keyId.toUtf8().data(), error, false); if (!error && !key.isNull()) { keys.push_back(key); } } auto encryptResult = encryptJob->exec(keys, plainText, false, cipherText); if (!isSuccess(encryptResult.error())) { Q_EMIT errorString(fromGpgmeCharStar(encryptResult.error().asString())); continue; } QSaveFile save(fileName); bool open = save.open(QIODevice::WriteOnly | QIODevice::Truncate); if (!open) { Q_EMIT errorString(QStringLiteral("open file failed %1").arg(fileName)); continue; } save.write(cipherText); if (save.error() != QFileDevice::NoError) { Q_EMIT errorString(QStringLiteral("Writing file failed: %1").arg(fileName)); continue; } else { save.commit(); } } } Q_EMIT endReencryptPath(); } void Pass::Move(const QString& src, const QString& dest, const bool force) { QFileInfo srcFileInfo(src); QFileInfo destFileInfo(dest); QString destFile; QString srcFileBaseName = srcFileInfo.fileName(); if (srcFileInfo.isFile()) { if (destFileInfo.isFile()) { if (!force) { return; } } else if (destFileInfo.isDir()) { destFile = QDir(dest).filePath(srcFileBaseName); } else { destFile = dest; } if (destFile.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive)) destFile.chop(4); // make sure suffix is lowercase destFile.append(QStringLiteral(".gpg")); } else if (srcFileInfo.isDir()) { if (destFileInfo.isDir()) { destFile = QDir(dest).filePath(srcFileBaseName); } else if (destFileInfo.isFile()) { return; } else { destFile = dest; } } else { return; } QDir qDir; if (force) { qDir.remove(destFile); } qDir.rename(src, destFile); } void Pass::Copy(const QString& src, const QString& dest, const bool force) { QFileInfo destFileInfo(dest); QDir qDir; if (force) { qDir.remove(dest); } if (!QFile::copy(src, dest)) { Q_EMIT errorString(QStringLiteral("Failed to copy file")); } // reecrypt all files under the new folder if (destFileInfo.isDir()) { reencryptPath(destFileInfo.absoluteFilePath()); } else if (destFileInfo.isFile()) { reencryptPath(destFileInfo.dir().path()); } } quint32 Pass::boundedRandom(quint32 bound) { if (bound < 2) { return 0; } quint32 randval; const quint32 max_mod_bound = (1 + ~bound) % bound; do { randval = QRandomGenerator::system()->generate(); } while (randval < max_mod_bound); return randval % bound; } QString Pass::generateRandomPassword(const QString &charset, unsigned int length) { QString out; for (unsigned int i = 0; i < length; ++i) { out.append(charset.at(static_cast(boundedRandom(static_cast(charset.length()))))); } return out; } diff --git a/src/storemodel.cpp b/src/storemodel.cpp index f22b2ac..1a7c334 100644 --- a/src/storemodel.cpp +++ b/src/storemodel.cpp @@ -1,276 +1,276 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2019 Maciej S. Szmigiero SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "storemodel.h" #include "pass.h" #include "util.h" #include #include #include #include #include #include -#include +#include static const QString mimeType = QStringLiteral("application/vnd+gnupgpass.dragAndDropInfoPasswordStore"); QDataStream &operator<<(QDataStream &out, const dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore) { out << dragAndDropInfoPasswordStore.isDir << dragAndDropInfoPasswordStore.isFile << dragAndDropInfoPasswordStore.path; return out; } QDataStream &operator>>(QDataStream &in, dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore) { in >> dragAndDropInfoPasswordStore.isDir >> dragAndDropInfoPasswordStore.isFile >> dragAndDropInfoPasswordStore.path; return in; } /** * @brief StoreModel::StoreModel * SubClass of QSortFilterProxyModel via * http://www.qtcentre.org/threads/46471-QTreeView-Filter */ StoreModel::StoreModel(Pass &pass) : m_pass(pass) { setRecursiveFilteringEnabled(true); } /** * @brief StoreModel::data don't show the .gpg at the end of a file. * @param index * @param role * @return */ QVariant StoreModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto initial_value = QSortFilterProxyModel::data(index, role); if (role == Qt::DisplayRole) { QString name = initial_value.toString(); name.replace(Util::endsWithGpg(), QString{}); return name; } return initial_value; } /** * @brief StoreModel::supportedDropActions enable drop. * @return */ Qt::DropActions StoreModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } /** * @brief StoreModel::supportedDragActions enable drag. * @return */ Qt::DropActions StoreModel::supportedDragActions() const { return Qt::CopyAction | Qt::MoveAction; } /** * @brief StoreModel::flags * @param index * @return */ Qt::ItemFlags StoreModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index); if (index.isValid()) { return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; } return Qt::ItemIsDropEnabled | defaultFlags; } /** * @brief StoreModel::mimeTypes * @return */ QStringList StoreModel::mimeTypes() const { QStringList types; types << mimeType; return types; } /** * @brief StoreModel::mimeData * @param indexes * @return */ QMimeData *StoreModel::mimeData(const QModelIndexList &indexes) const { dragAndDropInfoPasswordStore info; QByteArray encodedData; // only use the first, otherwise we should enable multiselection QModelIndex index = indexes.at(0); if (index.isValid()) { QModelIndex useIndex = mapToSource(index); info.isDir = fs()->fileInfo(useIndex).isDir(); info.isFile = fs()->fileInfo(useIndex).isFile(); info.path = fs()->fileInfo(useIndex).absoluteFilePath(); QDataStream stream(&encodedData, QIODevice::WriteOnly); stream << info; } auto *mimeData = new QMimeData(); mimeData->setData(mimeType, encodedData); return mimeData; } /** * @brief StoreModel::canDropMimeData * @param data * @param action * @param row * @param column * @param parent * @return */ bool StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const { #ifdef QT_DEBUG qDebug() << action << row; #else Q_UNUSED(action) Q_UNUSED(row) #endif if (!fs()) { return false; } QModelIndex useIndex = this->index(parent.row(), parent.column(), parent.parent()); QByteArray encodedData = data->data(mimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); dragAndDropInfoPasswordStore info; stream >> info; if (!data->hasFormat(mimeType)) return false; if (column > 0) { return false; } // you can drop a folder on a folder if (fs()->fileInfo(mapToSource(useIndex)).isDir() && info.isDir) { return true; } // you can drop a file on a folder if (fs()->fileInfo(mapToSource(useIndex)).isDir() && info.isFile) { return true; } // you can drop a file on a file if (fs()->fileInfo(mapToSource(useIndex)).isFile() && info.isFile) { return true; } return false; } /** * @brief StoreModel::dropMimeData * @param data * @param action * @param row * @param column * @param parent * @return */ bool StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (!canDropMimeData(data, action, row, column, parent)) return false; if (action == Qt::IgnoreAction) { return true; } QByteArray encodedData = data->data(mimeType); QDataStream stream(&encodedData, QIODevice::ReadOnly); dragAndDropInfoPasswordStore info; stream >> info; QModelIndex destIndex = this->index(parent.row(), parent.column(), parent.parent()); QFileInfo destFileinfo = fs()->fileInfo(mapToSource(destIndex)); QFileInfo srcFileInfo = QFileInfo(info.path); QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath()); QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath()); if (info.isDir) { // dropped dir onto dir if (destFileinfo.isDir()) { QDir destDir = QDir(cleanedDest).filePath(srcFileInfo.fileName()); QString cleanedDestDir = QDir::cleanPath(destDir.absolutePath()); if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDestDir); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDestDir); } } } else if (info.isFile) { // dropped file onto a directory if (destFileinfo.isDir()) { if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDest); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDest); } } else if (destFileinfo.isFile()) { // dropped file onto a file int answer = QMessageBox::question(nullptr, i18n("force overwrite?"), i18nc("overwrite DestinationFile with SourceFile","overwrite %1 with %2?", cleanedDest, cleanedSrc), QMessageBox::Yes | QMessageBox::No); bool force = answer == QMessageBox::Yes; if (action == Qt::MoveAction) { m_pass.Move(cleanedSrc, cleanedDest, force); } else if (action == Qt::CopyAction) { m_pass.Copy(cleanedSrc, cleanedDest, force); } } } return true; } /** * @brief StoreModel::lessThan * @param source_left * @param source_right * @return */ bool StoreModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { /* matches logic in QFileSystemModelSorter::compareNodes() */ #ifndef Q_OS_MAC if (fs() && (source_left.column() == 0 || source_left.column() == 1)) { bool leftD = fs()->isDir(source_left); bool rightD = fs()->isDir(source_right); if (leftD ^ rightD) return leftD; } #endif return QSortFilterProxyModel::lessThan(source_left, source_right); } QFileSystemModel* StoreModel::fs() const { return static_cast(sourceModel()); } diff --git a/src/usersdialog.cpp b/src/usersdialog.cpp index a261b26..469bdf5 100644 --- a/src/usersdialog.cpp +++ b/src/usersdialog.cpp @@ -1,182 +1,182 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "usersdialog.h" #include "pass.h" #include "ui_usersdialog.h" #include "ui_userswidget.h" #include #include #include #include -#include +#include void UsersWidgetData::generateUserList() { QList users = m_pass.listKeys(); QList selected_users; int count = 0; QStringList recipients = m_pass.getRecipientList(m_dir); if (!recipients.isEmpty()) selected_users = m_pass.listKeys(recipients); for (const UserInfo &sel : qAsConst(selected_users)) { for (auto &user : users) if (sel.key_id == user.key_id) user.enabled = true; } if (count > selected_users.size()) { // Some keys seem missing from keyring, add them separately const QStringList missingRecipients = m_pass.getRecipientList(m_dir.isEmpty() ? QString{} : m_dir); for (const QString &missingRecipient : qAsConst(recipients)) { if (m_pass.listKeys(missingRecipient).empty()) { UserInfo i; i.enabled = true; i.key_id = missingRecipient; i.name = QStringLiteral(" ?? ") + i18n("Key not found in keyring"); users.append(i); } } } m_userList = users; } void UsersWidgetData::init() { m_pass.Init(m_dir, m_userList); } void UsersWidgetData::setUi(Ui_UsersWidget *u) { this->ui = u; QObject::connect(ui->listWidget, &QListWidget::itemChanged, ui->listWidget, [this](auto item) { itemChange(item); }); QObject::connect(ui->checkBox, &QCheckBox::stateChanged, ui->checkBox, [this]() { populateList(); }); QObject::connect(ui->lineEdit, &QLineEdit::textChanged, ui->lineEdit, [this]() { populateList(); }); } /** * @brief UsersDialog::UsersDialog basic constructor * @param parent */ UsersDialog::UsersDialog(const QString& dir, Pass &pass, QWidget *parent) : QDialog(parent) , dialogUi(std::make_unique()) , ui(std::make_unique()) , d(pass) { d.m_dir = dir; dialogUi->setupUi(this); ui->setupUi(dialogUi->widget); d.generateUserList(); d.setUi(ui.get()); if (d.m_userList.isEmpty()) { QMessageBox::critical(parent, i18n("Keylist missing"), i18n("Could not fetch list of available GPG keys")); return; } d.populateList(); connect(dialogUi->buttonBox, &QDialogButtonBox::accepted, this, &UsersDialog::accept); connect(dialogUi->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); ui->lineEdit->setClearButtonEnabled(true); } /** * @brief UsersDialog::~UsersDialog basic destructor. */ UsersDialog::~UsersDialog() = default; Q_DECLARE_METATYPE(UserInfo *) /** * @brief UsersDialog::accept */ void UsersDialog::accept() { d.init(); QDialog::accept(); } /** * @brief UsersDialog::itemChange update the item information. * @param item */ void UsersWidgetData::itemChange(QListWidgetItem *item) { if (!item) return; auto *info = item->data(Qt::UserRole).value(); if (!info) return; info->enabled = item->checkState() == Qt::Checked; } /** * @brief UsersDialog::populateList update the view based on filter options * (such as searching). * @param filter */ void UsersWidgetData::populateList() { const auto filter = ui->lineEdit->text(); QRegularExpression nameFilter(QRegularExpression::wildcardToRegularExpression(QStringLiteral("*") + filter + QStringLiteral("*")), QRegularExpression::CaseInsensitiveOption); ui->listWidget->clear(); if (!m_userList.isEmpty()) { for (auto &user : m_userList) { if (filter.isEmpty() || nameFilter.match(user.name).hasMatch()) { if (!user.isValid() && !ui->checkBox->isChecked()) continue; if (user.expiry.toSecsSinceEpoch() > 0 && user.expiry.daysTo(QDateTime::currentDateTime()) > 0 && !ui->checkBox->isChecked()) continue; QString userText = user.name + QStringLiteral("\n") + user.key_id; if (user.created.toSecsSinceEpoch() > 0) { userText += QStringLiteral(" ") + i18nc("time of key creation","created %1", QLocale::system().toString(user.created, QLocale::ShortFormat)); } if (user.expiry.toSecsSinceEpoch() > 0) userText += QStringLiteral(" ") + i18nc("Time of key expiry","expires %1", QLocale::system().toString(user.expiry, QLocale::ShortFormat)); auto *item = new QListWidgetItem(userText, ui->listWidget); item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked); item->setData(Qt::UserRole, QVariant::fromValue(&user)); if (user.have_secret) { // item->setForeground(QColor(32, 74, 135)); item->setForeground(Qt::blue); QFont font; font.setFamily(font.defaultFamily()); font.setBold(true); item->setFont(font); } else if (!user.isValid()) { item->setBackground(QColor(164, 0, 0)); item->setForeground(Qt::white); } else if (user.expiry.toSecsSinceEpoch() > 0 && user.expiry.daysTo(QDateTime::currentDateTime()) > 0) { item->setForeground(QColor(164, 0, 0)); } else if (!user.fullyValid()) { item->setBackground(QColor(164, 80, 0)); item->setForeground(Qt::white); } ui->listWidget->addItem(item); } } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 69f5128..d80d4ca 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,2 +1,2 @@ -ecm_add_test(auto/ui/tst_ui.cpp LINK_LIBRARIES gpgpass_internal Qt5::Test) -ecm_add_test(auto/util/tst_util.cpp LINK_LIBRARIES gpgpass_internal Qt5::Test) +ecm_add_test(auto/ui/tst_ui.cpp LINK_LIBRARIES gpgpass_internal Qt6::Test) +ecm_add_test(auto/util/tst_util.cpp LINK_LIBRARIES gpgpass_internal Qt6::Test) diff --git a/tests/auto/util/tst_util.cpp b/tests/auto/util/tst_util.cpp index b5e0db1..5bc13a3 100644 --- a/tests/auto/util/tst_util.cpp +++ b/tests/auto/util/tst_util.cpp @@ -1,121 +1,121 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-License-Identifier: GPL-3.0-or-later */ #include "filecontent.h" #include "util.h" #include #include #include /** * @brief The tst_util class is our first unit test */ class tst_util : public QObject { Q_OBJECT public: tst_util(); ~tst_util() override; public Q_SLOTS: void init(); void cleanup(); private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void normalizeFolderPath(); void fileContent(); }; bool operator==(const NamedValue &a, const NamedValue &b) { return a.name == b.name && a.value == b.value; } /** * @brief tst_util::tst_util basic constructor */ tst_util::tst_util() = default; /** * @brief tst_util::~tst_util basic destructor */ tst_util::~tst_util() = default; /** * @brief tst_util::init unit test init method */ void tst_util::init() { } /** * @brief tst_util::cleanup unit test cleanup method */ void tst_util::cleanup() { } /** * @brief tst_util::initTestCase test case init method */ void tst_util::initTestCase() { } /** * @brief tst_util::cleanupTestCase test case cleanup method */ void tst_util::cleanupTestCase() { } /** * @brief tst_util::normalizeFolderPath test to check correct working * of Util::normalizeFolderPath the paths should always end with a slash */ void tst_util::normalizeFolderPath() { QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test")), QStringLiteral("test/")); QCOMPARE(Util::normalizeFolderPath(QStringLiteral("test/")), QStringLiteral("test/")); } void tst_util::fileContent() { NamedValue key = {QStringLiteral("key"), QStringLiteral("val")}; NamedValue key2 = {QStringLiteral("key2"), QStringLiteral("val2")}; QString password = QStringLiteral("password"); FileContent fc = FileContent::parse(QStringLiteral("password\n"), {}, false); QCOMPARE(fc.getPassword(), password); QCOMPARE(fc.getNamedValues(), {}); QCOMPARE(fc.getRemainingData(), QString()); fc = FileContent::parse(QStringLiteral("password"), {}, false); QCOMPARE(fc.getPassword(), password); QCOMPARE(fc.getNamedValues(), {}); QCOMPARE(fc.getRemainingData(), QString()); fc = FileContent::parse(QStringLiteral("password\nfoobar\n"), {}, false); QCOMPARE(fc.getPassword(), password); QCOMPARE(fc.getNamedValues(), {}); QCOMPARE(fc.getRemainingData(), QStringLiteral("foobar\n")); fc = FileContent::parse(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, false); QCOMPARE(fc.getPassword(), password); - QCOMPARE(fc.getNamedValues(), {key2}); + QCOMPARE(fc.getNamedValues(), QList{key2}); QCOMPARE(fc.getRemainingData(), QStringLiteral("key: val")); fc = FileContent::parse(QStringLiteral("password\nkey: val\nkey2: val2"), {QStringLiteral("key2")}, true); QCOMPARE(fc.getPassword(), password); QCOMPARE(fc.getNamedValues(), NamedValues({key, key2})); QCOMPARE(fc.getRemainingData(), QString()); } QTEST_MAIN(tst_util) #include "tst_util.moc"