diff --git a/.kde-ci.yml b/.kde-ci.yml index d46dd2f..8ad6469 100644 --- a/.kde-ci.yml +++ b/.kde-ci.yml @@ -1,17 +1,19 @@ # SPDX-FileCopyrightText: None # SPDX-License-Identifier: CC0-1.0 Dependencies: - 'on': ['@all'] 'require': 'frameworks/extra-cmake-modules': '@stable' 'frameworks/ki18n' : '@stable' 'frameworks/kconfigwidgets' : '@stable' 'frameworks/kiconthemes' : '@stable' 'frameworks/prison' : '@stable' Options: test-before-installing: True require-passing-tests-on: [ 'Linux', 'Windows' ] cppcheck-ignore-files: - tests + cppcheck-arguments: + - --suppress=useStlAlgorithm diff --git a/src/clipboardhelper.h b/src/clipboardhelper.h index 3390260..b5f8641 100644 --- a/src/clipboardhelper.h +++ b/src/clipboardhelper.h @@ -1,45 +1,45 @@ /* 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 */ #ifndef CLIPBOARDHELPER_H #define CLIPBOARDHELPER_H #include #include class QMainWindow; /** * Helper for Clipboard interactions * * This should be the only file where clipboard interactions * should happen. This ensures we can attempt to clear it after some * interval to try help avoid leaking passwords thru the clipboard */ class ClipboardHelper : public QObject { Q_OBJECT public: - ClipboardHelper(QMainWindow *mainWindow); + explicit ClipboardHelper(QMainWindow *mainWindow); ~ClipboardHelper(); void setClippedText(const QString &); void clearClippedText(); void setClipboardTimer(); private: QMainWindow *m_mainWindow; QTimer clearClipboardTimer; QString clippedText; public Q_SLOTS: void clearClipboard(); void copyTextToClipboard(const QString &text); }; #endif // CLIPBOARDHELPER_H diff --git a/src/configdialog.cpp b/src/configdialog.cpp index f053c7b..89777b0 100644 --- a/src/configdialog.cpp +++ b/src/configdialog.cpp @@ -1,311 +1,302 @@ /* 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 /** * @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->storePath->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()); useTemplate(Settings::isUseTemplate()); ui->profileTable->verticalHeader()->hide(); ui->profileTable->horizontalHeader()->setStretchLastSection(true); ui->label->setText(ui->label->text() + QString::fromUtf8(GPGPASS_VERSION_STRING)); connect(ui->profileTable, &QTableWidget::itemChanged, this, &ConfigDialog::onProfileTableItemChanged); connect(this, &ConfigDialog::accepted, this, &ConfigDialog::on_accepted); connect(ui->toolButtonStore, &QAbstractButton::clicked, this, &ConfigDialog::selectStoreFolder); connect(ui->checkBoxUseTemplate, &QAbstractButton::toggled, this, &ConfigDialog::toggleTemplateSubentries); connect(ui->checkBoxAutoclearPanel, &QAbstractButton::toggled, this, &ConfigDialog::toggleAutoClearPanelSubentries); connect(ui->addButton, &QAbstractButton::clicked, this, &ConfigDialog::addProfile); connect(ui->deleteButton, &QAbstractButton::clicked, this, &ConfigDialog::deleteSelectedProfile); } ConfigDialog::~ConfigDialog() = default; void ConfigDialog::validate(QTableWidgetItem *item) { bool status = true; if (item == nullptr) { for (int i = 0; i < ui->profileTable->rowCount(); i++) { for (int j = 0; j < ui->profileTable->columnCount(); j++) { QTableWidgetItem *_item = ui->profileTable->item(i, j); if (_item->text().isEmpty()) { _item->setBackground(Qt::red); status = false; break; } } if (!status) break; } } else { if (item->text().isEmpty()) { item->setBackground(Qt::red); status = false; } } ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status); } void ConfigDialog::on_accepted() { Settings::setPassStore(Util::normalizeFolderPath(ui->storePath->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()) // TODO(annejan) call check ui->storePath->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); } /** * @brief ConfigDialog::setProfiles set the profiles and chosen profile from * MainWindow. * @param profiles * @param profile */ -void ConfigDialog::setProfiles(QHash profiles, QString profile) +void ConfigDialog::setProfiles(const QHash& profiles, const QString& profile) { - if (profiles.contains(QString{})) { - profiles.remove({}); - // remove weird "" key value pairs - } - - ui->profileTable->setRowCount(profiles.count()); + ui->profileTable->clear(); + ui->profileTable->setSortingEnabled(false); QHashIterator i(profiles); - int n = 0; while (i.hasNext()) { i.next(); if (!i.value().isEmpty() && !i.key().isEmpty()) { - ui->profileTable->setItem(n, 0, new QTableWidgetItem(i.key())); - ui->profileTable->setItem(n, 1, new QTableWidgetItem(i.value())); - if (i.key() == profile) - ui->profileTable->selectRow(n); + ui->profileTable->insertRow(0); + ui->profileTable->setItem(0, 0, new QTableWidgetItem(i.key())); + ui->profileTable->setItem(0, 1, new QTableWidgetItem(i.value())); } - ++n; } + 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() { + ui->profileTable->setSortingEnabled(false); int n = ui->profileTable->rowCount(); ui->profileTable->insertRow(n); ui->profileTable->setItem(n, 0, new QTableWidgetItem()); ui->profileTable->setItem(n, 1, new QTableWidgetItem(ui->storePath->text())); ui->profileTable->selectRow(n); ui->deleteButton->setEnabled(true); + ui->profileTable->setSortingEnabled(true); validate(); } /** * @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); validate(); } -/** - * @brief ConfigDialog::criticalMessage weapper for showing critical messages - * in a popup. - * @param title - * @param text - */ -void ConfigDialog::criticalMessage(const QString &title, const QString &text) -{ - QMessageBox::critical(this, title, text, QMessageBox::Ok, QMessageBox::Ok); -} - 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); } void ConfigDialog::onProfileTableItemChanged(QTableWidgetItem *item) { validate(item); } /** * @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/configdialog.h b/src/configdialog.h index 9d4fb64..c85d18c 100644 --- a/src/configdialog.h +++ b/src/configdialog.h @@ -1,71 +1,68 @@ /* 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 */ #ifndef CONFIGDIALOG_H_ #define CONFIGDIALOG_H_ #include "passwordconfiguration.h" #include namespace Ui { struct UserInfo; class ConfigDialog; } // namespace Ui /*! \class ConfigDialog \brief The ConfigDialog handles the configuration interface. This class should also take the handling from the MainWindow class. */ class MainWindow; class QCloseEvent; class QTableWidgetItem; class ConfigDialog : public QDialog { Q_OBJECT public: explicit ConfigDialog(MainWindow *parent); ~ConfigDialog(); void useAutoclear(bool useAutoclear); void useAutoclearPanel(bool useAutoclearPanel); QHash getProfiles(); void setPasswordConfiguration(const PasswordConfiguration &config); PasswordConfiguration getPasswordConfiguration(); void useTemplate(bool useTemplate); private Q_SLOTS: void on_accepted(); void selectStoreFolder(); void addProfile(); void deleteSelectedProfile(); void toggleAutoClearPanelSubentries(bool enable); void toggleTemplateSubentries(bool enable); void onProfileTableItemChanged(QTableWidgetItem *item); private: QScopedPointer ui; - void setProfiles(QHash, QString); + void setProfiles(const QHash&, const QString&); QString selectFolder(); - // QMessageBox::critical with hack to avoid crashes with - // Qt 5.4.1 when QApplication::exec was not yet called - void criticalMessage(const QString &title, const QString &text); void validate(QTableWidgetItem *item = nullptr); MainWindow *mainWindow; }; #endif // CONFIGDIALOG_H_ diff --git a/src/firsttimedialog.cpp b/src/firsttimedialog.cpp index ac06acc..e337ea5 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 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.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.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 (auto &item : std::as_const(d.m_userList)) { + 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/firsttimedialog.h b/src/firsttimedialog.h index 3efdd64..759046a 100644 --- a/src/firsttimedialog.h +++ b/src/firsttimedialog.h @@ -1,107 +1,107 @@ /* SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef FIRSTTIMEDIALOG_H #define FIRSTTIMEDIALOG_H #include "usersdialog.h" #include #include #include class Ui_KeyGenWidget; class Ui_UsersWidget; class Ui_KeyGenWidget; class Ui_SelectPasswordStore; class DialogState : public QObject { Q_OBJECT public: - DialogState(Pass &p); + explicit DialogState(Pass &p); QList privateKeys() const; QGpgME::Protocol *protocol = QGpgME::openpgp(); QString storePath; Pass &pass; }; class FirstTimeDialog : public QWizard { enum Pages { Intro, KeyGen, PasswordStore, KeySelect, Done }; Q_OBJECT public: FirstTimeDialog(QWidget *mainWindow, Pass &pass); int nextId() const override; void done(int i) override; private: QWidget *m_mainWindow; DialogState m_state; }; class IntroPage : public QWizardPage { Q_OBJECT public: - IntroPage(DialogState &s); + explicit IntroPage(DialogState &s); private: DialogState &m_state; }; class KeyGenPage : public QWizardPage { Q_OBJECT public: - KeyGenPage(DialogState &s); + explicit KeyGenPage(DialogState &s); bool isComplete() const override; ~KeyGenPage(); private Q_SLOTS: void startKeyGen(); void checkEntries(); private: DialogState &m_state; std::unique_ptr m_ui; }; class PasswordStorePage : public QWizardPage { Q_OBJECT public: - PasswordStorePage(DialogState &s); + explicit PasswordStorePage(DialogState &s); bool validatePage() override; ~PasswordStorePage(); bool isComplete() const override; private: std::unique_ptr m_ui; DialogState &m_state; }; class KeySelectPage : public QWizardPage { Q_OBJECT public: - KeySelectPage(DialogState &s); + explicit KeySelectPage(DialogState &s); bool isComplete() const override; bool validatePage() override; void initializePage() override; ~KeySelectPage(); private: std::unique_ptr m_ui; DialogState &m_state; UsersWidgetData d; }; class DonePage : public QWizardPage { public: DonePage(); }; #endif // FIRSTTIMEDIALOG_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f4a614e..2a96302 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,961 +1,959 @@ /* 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 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); connect(m_pass.get(), &Pass::errorString, this, [this](auto str) { flashText(str, true); 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); } 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 = true; // 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())); } 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(ui->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::flashText(const QString &text, const bool isError, const bool isHtml) { if (isError) ui->textBrowser->setTextColor(Qt::red); if (isHtml) { QString _text = text; if (!ui->textBrowser->toPlainText().isEmpty()) _text = ui->textBrowser->toHtml() + _text; ui->textBrowser->setHtml(_text); } else { ui->textBrowser->setText(text); ui->textBrowser->setTextColor(Qt::black); } } /** * @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(false); 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(false); } 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: clearTemplateWidgets(); // 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(0, i18n("Password"), password); } NamedValues namedValues = fileContent.getNamedValues(); for (int j = 0; j < namedValues.length(); ++j) { NamedValue nv = namedValues.at(j); addToGridLayout(j + 1, nv.name, nv.value); } if (ui->gridLayout->count() == 0) ui->verticalLayoutPassword->setSpacing(0); else ui->verticalLayoutPassword->setSpacing(6); output = fileContent.getRemainingDataForDisplay(); } if (Settings::isUseAutoclearPanel()) { clearPanelTimer.start(); } output = output.toHtmlEscaped(); output.replace(Util::protocolRegex(), QStringLiteral(R"(\1)")); output.replace(QStringLiteral("\n"), QStringLiteral("
")); flashText(output, false, true); setUiElementsEnabled(true); } /** * @brief MainWindow::clearPanel hide the information from shoulder surfers */ void MainWindow::clearPanel(bool notify) { while (ui->gridLayout->count() > 0) { QLayoutItem *item = ui->gridLayout->takeAt(0); delete item->widget(); delete item; } if (notify) { QString output = i18n("*** Password and Content hidden ***"); ui->textBrowser->setHtml(output); } else { 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(false); 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); 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(); 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() + QDir::separator() + file, QDirIterator::Subdirectories); - bool okDir = true; - while (it.hasNext() && okDir) { + while (it.hasNext()) { it.next(); if (auto fi = it.fileInfo(); fi.isFile()) { if (fi.suffix() != QStringLiteral("gpg")) { - okDir = false; 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) { QFileInfo fi(dir); if (!fi.isDir()) { dir = fi.absolutePath(); } UsersDialog d(dir, *m_pass, this); if (!d.exec()) { ui->treeView->setFocus(); } } /** * @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(); } } 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()) { ui->profileWidget->hide(); } else { ui->profileWidget->show(); ui->profileBox->setEnabled(profiles.size() > 1); ui->profileBox->clear(); QHashIterator i(profiles); while (i.hasNext()) { i.next(); if (!i.key().isEmpty()) ui->profileBox->addItem(i.key()); } ui->profileBox->model()->sort(0); } int index = ui->profileBox->findText(Settings::getProfile()); if (index != -1) // -1 for not found ui->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(); 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()))); ui->actionEdit->setEnabled(false); ui->actionDelete->setEnabled(false); } /** * @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()) { - QAction *addFolder = contextMenu.addAction(i18n("Add folder")); - QAction *addPassword = contextMenu.addAction(i18n("Add password")); - QAction *users = contextMenu.addAction(i18n("Users")); - connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder); - connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword); - connect(users, &QAction::triggered, this, &MainWindow::onUsers); + 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()) { - QAction *edit = contextMenu.addAction(i18n("Edit")); + const QAction *edit = contextMenu.addAction(i18n("Edit")); connect(edit, &QAction::triggered, this, &MainWindow::onEdit); } if (selected) { contextMenu.addSeparator(); if (fileOrFolder.isDir()) { - QAction *renameFolder = contextMenu.addAction(i18n("Rename folder")); - connect(renameFolder, &QAction::triggered, this, &MainWindow::renameFolder); + const QAction *renameFolderAction = contextMenu.addAction(i18n("Rename folder")); + connect(renameFolderAction, &QAction::triggered, this, &MainWindow::renameFolder); } else if (fileOrFolder.isFile()) { - QAction *renamePassword = contextMenu.addAction(i18n("Rename password")); - connect(renamePassword, &QAction::triggered, this, &MainWindow::renamePassword); + const QAction *renamePasswordAction = contextMenu.addAction(i18n("Rename password")); + connect(renamePasswordAction, &QAction::triggered, this, &MainWindow::renamePassword); } - QAction *deleteItem = contextMenu.addAction(i18n("Delete")); + 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->gridLayout->count() > 0) { QLayoutItem *item = ui->gridLayout->takeAt(0); delete item->widget(); delete item; } ui->verticalLayoutPassword->setSpacing(0); } /** * @brief MainWindow::addToGridLayout add a field to the template grid * @param position * @param field * @param value */ void MainWindow::addToGridLayout(int position, 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")), m_clipboardHelper, [this, trimmedValue] { m_clipboardHelper->copyTextToClipboard(trimmedValue); }); frame->layout()->addWidget(fieldLabel.release()); auto qrButton = createPushButton(QIcon::fromTheme(QStringLiteral("view-barcode-qr")), m_clipboardHelper, [this, trimmedValue]() { auto barcode = Prison::createBarcode(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, 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(0, 0, 0, 0); frame->layout()->addWidget(line); } // set into the layout ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0); ui->gridLayout->addWidget(frame, position, 1); } /** * @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(QString title, QString msg) { QMessageBox::critical(this, title, msg); } diff --git a/src/pass.cpp b/src/pass.cpp index 53efa14..13e9f24 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -1,415 +1,436 @@ /* 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 +// TODO remove +#include + using namespace std; // TODO remove -static QString ensurePassStore(const QString &file, const char *location) +Q_REQUIRED_RESULT static bool ensurePassStore(const QString &file, const char *location) { if (file.startsWith(Settings::getPassStore())) { - return file; + return true; } - qWarning() << "file not in pass store called from" << location << file << Settings::getPassStore(); - return Settings::getPassStore() + QStringLiteral("/") + file; + 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 -static QString ensureSuffix(const QString &file, const char *location) +Q_REQUIRED_RESULT static bool ensureSuffix(const QString &file, const char *location) { if (file.endsWith(QStringLiteral(".gpg"))) { - return file; + return true; } qWarning() << "file without suffix called from " << location; - return file + QStringLiteral(".gpg"); + 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(QStringList keystrings, bool secret) +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.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.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(QString keystring, bool secret) +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(QString for_file) +QStringList Pass::getRecipientList(const QString& for_file) { - QDir gpgIdPath(QFileInfo(ensurePassStore(for_file, Q_FUNC_INFO)).absoluteDir()); + 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(QString file) +void Pass::Show(const QString& file) { - file = ensureSuffix(ensurePassStore(file, Q_FUNC_INFO), Q_FUNC_INFO); + 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 processErrorExit(result.error().code(), QString::fromLocal8Bit(result.error().asString())); } // Q_EMIT log(auditLog); Q_UNUSED(logError); Q_UNUSED(auditLog); }); job->start(data); } -void Pass::Insert(QString file, QString newValue) +void Pass::Insert(const QString& file, const QString& newValue) { - file = ensureSuffix(ensurePassStore(file, Q_FUNC_INFO), Q_FUNC_INFO); + 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(QString file, bool isDir) +void Pass::Remove(const QString& file, bool isDir) { - file = ensurePassStore(file, Q_FUNC_INFO); + if (!ensurePassStore(file, Q_FUNC_INFO)) { + return; + } if (!isDir) { - file = ensureSuffix(file, Q_FUNC_INFO); + if(!ensureSuffix(file, Q_FUNC_INFO)) { + return; + } QFile(file).remove(); } else { QDir dir(file); dir.removeRecursively(); } } -void Pass::Init(QString path, const QList &users) +void Pass::Init(const QString& path, const QList &users) { - QString gpgIdFile = ensurePassStore(path, Q_FUNC_INFO) + QStringLiteral(".gpg-id"); + 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(ensurePassStore(dir, Q_FUNC_INFO), QStringList() << QStringLiteral("*.gpg"), QDir::Files, QDirIterator::Subdirectories); + 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) +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) +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/pass.h b/src/pass.h index 5882295..dc09f2c 100644 --- a/src/pass.h +++ b/src/pass.h @@ -1,66 +1,63 @@ /* 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 */ #ifndef PASS_H #define PASS_H #include "userinfo.h" #include #include #include #include #include /*! \class Pass \brief Acts as an abstraction for pass or pass imitation */ class Pass final : public QObject { Q_OBJECT public: Pass(); - ~Pass() - { - } - void Show(QString file); - void Insert(QString file, QString value); - void Remove(QString file, bool isDir); - void Move(const QString srcDir, const QString dest, const bool force = false); - void Copy(const QString srcDir, const QString dest, const bool force = false); - void Init(QString path, const QList &users); + void Show(const QString& file); + void Insert(const QString& file, const QString& value); + void Remove(const QString& file, bool isDir); + void Move(const QString& srcDir, const QString& dest, const bool force = false); + void Copy(const QString& srcDir, const QString& dest, const bool force = false); + void Init(const QString &path, const QList &users); QString Generate_b(unsigned int length, const QString &charset); - QList listKeys(QStringList keystrings, bool secret = false); - QList listKeys(QString keystring = QString{}, bool secret = false); - static QStringList getRecipientList(QString for_file); + QList listKeys(const QStringList& keystrings, bool secret = false); + QList listKeys(const QString& keystring = QString{}, bool secret = false); + static QStringList getRecipientList(const QString& for_file); private: QGpgME::Protocol *protocol = QGpgME::openpgp(); QString generateRandomPassword(const QString &charset, unsigned int length); quint32 boundedRandom(quint32 bound); void reencryptPath(const QString &dir); Q_SIGNALS: void startReencryptPath(); void endReencryptPath(); void errorString(const QString &err); - void statusMsg(QString, int); - void critical(QString, QString); + void statusMsg(const QString&, int); + void critical(const QString&, const QString&); void processErrorExit(int exitCode, const QString &err); void finishedShow(const QString &plainTextPassword); void finishedInsert(); }; #endif // PASS_H diff --git a/src/passworddialog.cpp b/src/passworddialog.cpp index 4a89a88..22e8b8c 100644 --- a/src/passworddialog.cpp +++ b/src/passworddialog.cpp @@ -1,241 +1,241 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Lukas Vogel 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 "passworddialog.h" #include "filecontent.h" #include "pass.h" #include "passwordconfiguration.h" #include "settings.h" #include "ui_passworddialog.h" #include #include /** * @brief PasswordDialog::PasswordDialog basic constructor. * @param passConfig configuration constant * @param parent */ PasswordDialog::PasswordDialog(Pass &pass, const PasswordConfiguration &passConfig, QWidget *parent) : QDialog(parent) , ui(std::make_unique()) , m_pass(pass) , m_passConfig(passConfig) { m_templating = false; m_allFields = false; m_isNew = false; ui->setupUi(this); setLength(m_passConfig.length); setPasswordCharTemplate(m_passConfig.selected); connect(&m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass); } /** * @brief PasswordDialog::PasswordDialog complete constructor. * @param file * @param isNew * @param parent pointer */ PasswordDialog::PasswordDialog(Pass &pass, const QString &file, const bool &isNew, QWidget *parent) : QDialog(parent) , ui(std::make_unique()) , m_pass(pass) , m_file(file) , m_isNew(isNew) { if (!isNew) m_pass.Show(m_file); ui->setupUi(this); setWindowTitle(this->windowTitle() + QStringLiteral(" ") + m_file); m_passConfig = Settings::getPasswordConfiguration(); setTemplate(Settings::getPassTemplate(), Settings::isUseTemplate()); templateAll(Settings::isTemplateAllFields()); setLength(m_passConfig.length); setPasswordCharTemplate(m_passConfig.selected); connect(&m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass); connect(&m_pass, &Pass::processErrorExit, this, &PasswordDialog::close); connect(this, &PasswordDialog::accepted, this, &PasswordDialog::dialogAccepted); connect(this, &PasswordDialog::rejected, this, &PasswordDialog::dialogCancelled); connect(ui->createPasswordButton, &QAbstractButton::clicked, this, &PasswordDialog::createPassword); connect(ui->checkBoxShow, &QCheckBox::stateChanged, this, &PasswordDialog::showPasswordChanged); } /** * @brief Pass{}{}wordDialog::~PasswordDialog basic destructor. */ PasswordDialog::~PasswordDialog() = default; /** * @brief PasswordDialog::on_checkBoxShow_stateChanged hide or show passwords. * @param arg1 */ void PasswordDialog::showPasswordChanged(int arg1) { if (arg1) ui->lineEditPassword->setEchoMode(QLineEdit::Normal); else ui->lineEditPassword->setEchoMode(QLineEdit::Password); } /** * @brief PasswordDialog::on_createPasswordButton_clicked generate a random * passwords. * @todo refactor when process is untangled from MainWindow class. */ void PasswordDialog::createPassword() { ui->widget->setEnabled(false); QString newPass = m_pass.Generate_b(static_cast(ui->spinBox_pwdLength->value()), m_passConfig.Characters[static_cast(ui->passwordTemplateSwitch->currentIndex())]); if (newPass.length() > 0) ui->lineEditPassword->setText(newPass); ui->widget->setEnabled(true); } /** * @brief PasswordDialog::on_accepted handle Ok click for QDialog */ void PasswordDialog::dialogAccepted() { QString newValue = getPassword(); if (newValue.isEmpty()) return; if (newValue.right(1) != QLatin1Char('\n')) newValue += QLatin1Char('\n'); m_pass.Insert(m_file, newValue); } /** * @brief PasswordDialog::on_rejected handle Cancel click for QDialog */ void PasswordDialog::dialogCancelled() { setPassword(QString()); } /** * @brief PasswordDialog::setPassword populate the (templated) fields. * @param password */ -void PasswordDialog::setPassword(QString password) +void PasswordDialog::setPassword(const QString& password) { FileContent fileContent = FileContent::parse(password, m_templating ? m_fields : QStringList(), m_allFields); ui->lineEditPassword->setText(fileContent.getPassword()); QWidget *previous = ui->checkBoxShow; // first set templated values NamedValues namedValues = fileContent.getNamedValues(); for (QLineEdit *line : qAsConst(templateLines)) { line->setText(namedValues.takeValue(line->objectName())); previous = line; } // show remaining values (if there are) otherLines.clear(); for (const NamedValue &nv : qAsConst(namedValues)) { auto *line = new QLineEdit(); line->setObjectName(nv.name); line->setText(nv.value); ui->formLayout->addRow(new QLabel(nv.name), line); setTabOrder(previous, line); otherLines.append(line); previous = line; } ui->plainTextEdit->insertPlainText(fileContent.getRemainingData()); } /** * @brief PasswordDialog::getPassword join the (templated) fields to a QString * for writing back. * @return collappsed password. */ QString PasswordDialog::getPassword() { QString passFile = ui->lineEditPassword->text() + QLatin1Char('\n'); QList allLines(templateLines); allLines.append(otherLines); for (QLineEdit *line : allLines) { QString text = line->text(); if (text.isEmpty()) continue; passFile += line->objectName() + QStringLiteral(": ") + text + QLatin1Char('\n'); } passFile += ui->plainTextEdit->toPlainText(); return passFile; } /** * @brief PasswordDialog::setTemplate set the template and create the fields. * @param rawFields */ -void PasswordDialog::setTemplate(QString rawFields, bool useTemplate) +void PasswordDialog::setTemplate(const QString& rawFields, bool useTemplate) { m_fields = rawFields.split(QLatin1Char('\n')); m_templating = useTemplate; templateLines.clear(); if (m_templating) { QWidget *previous = ui->checkBoxShow; for (const QString &field : qAsConst(m_fields)) { if (field.isEmpty()) continue; auto *line = new QLineEdit(); line->setObjectName(field); ui->formLayout->addRow(new QLabel(field), line); setTabOrder(previous, line); templateLines.append(line); previous = line; } } } /** * @brief PasswordDialog::templateAll basic setter for use in * PasswordDialog::setPassword templating all tokenisable lines. * @param templateAll */ void PasswordDialog::templateAll(bool templateAll) { m_allFields = templateAll; } /** * @brief PasswordDialog::setLength * PasswordDialog::setLength password length. * @param l */ void PasswordDialog::setLength(int l) { ui->spinBox_pwdLength->setValue(l); } /** * @brief PasswordDialog::setPasswordCharTemplate * PasswordDialog::setPasswordCharTemplate chose the template style. * @param t */ void PasswordDialog::setPasswordCharTemplate(int t) { ui->passwordTemplateSwitch->setCurrentIndex(t); } void PasswordDialog::setPass(const QString &output) { setPassword(output); // TODO(bezet): enable ui } diff --git a/src/passworddialog.h b/src/passworddialog.h index 6769d0e..71b5c1f 100644 --- a/src/passworddialog.h +++ b/src/passworddialog.h @@ -1,84 +1,84 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef PASSWORDDIALOG_H_ #define PASSWORDDIALOG_H_ #include "passwordconfiguration.h" #include namespace Ui { class PasswordDialog; } class QLineEdit; class QWidget; class Pass; /*! \class PasswordDialog \brief PasswordDialog Handles the inserting and editing of passwords. Includes templated views. */ class PasswordDialog : public QDialog { Q_OBJECT public: explicit PasswordDialog(Pass &pass, const PasswordConfiguration &passConfig, QWidget *parent = nullptr); PasswordDialog(Pass &pass, const QString &file, const bool &isNew, QWidget *parent = nullptr); ~PasswordDialog(); /*! Sets content in the password field in the interface. \param password the password as a QString \sa getPassword */ - void setPassword(QString password); + void setPassword(const QString& password); /*! Returns the password as set in the password field in the interface. \return password as a QString \sa setPassword */ QString getPassword(); /*! Sets content in the template for the interface. \param rawFields is the template as a QString \param useTemplate whether the template is used */ - void setTemplate(QString rawFields, bool useTemplate); + void setTemplate(const QString& rawFields, bool useTemplate); void templateAll(bool templateAll); void setLength(int l); void setPasswordCharTemplate(int t); public Q_SLOTS: void setPass(const QString &output); private Q_SLOTS: void showPasswordChanged(int arg1); void createPassword(); void dialogAccepted(); void dialogCancelled(); private: std::unique_ptr ui; Pass &m_pass; PasswordConfiguration m_passConfig; QStringList m_fields; QString m_file; bool m_templating; bool m_allFields; bool m_isNew; QList templateLines; QList otherLines; }; #endif // PASSWORDDIALOG_H_ diff --git a/src/storemodel.h b/src/storemodel.h index 7fcaddd..180c66e 100644 --- a/src/storemodel.h +++ b/src/storemodel.h @@ -1,56 +1,56 @@ /* 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 */ #ifndef STOREMODEL_H_ #define STOREMODEL_H_ #include /*! \class StoreModel \brief The QSortFilterProxyModel for handling filesystem searches. */ class QFileSystemModel; class Pass; class StoreModel : public QSortFilterProxyModel { Q_OBJECT private: QFileSystemModel *fs() const; Pass &m_pass; public: - StoreModel(Pass &pass); + explicit StoreModel(Pass &pass); QVariant data(const QModelIndex &index, int role) const override; bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; // QAbstractItemModel interface public: Qt::DropActions supportedDropActions() const override; Qt::DropActions supportedDragActions() const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList &indexes) const override; bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; }; /*! \struct dragAndDropInfo \brief holds values to share beetween drag and drop on the passwordstorage view */ struct dragAndDropInfoPasswordStore { - bool isDir; - bool isFile; + bool isDir = false; + bool isFile = false; QString path; }; #endif // STOREMODEL_H_ diff --git a/src/usersdialog.cpp b/src/usersdialog.cpp index 4abe4e6..a261b26 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 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 recipients = m_pass.getRecipientList(m_dir.isEmpty() ? QString{} : m_dir); - for (const QString &recipient : recipients) { - if (m_pass.listKeys(recipient).empty()) { + 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 = recipient; + 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(QString dir, Pass &pass, QWidget *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/src/usersdialog.h b/src/usersdialog.h index 169a6a6..4a2ddbf 100644 --- a/src/usersdialog.h +++ b/src/usersdialog.h @@ -1,65 +1,65 @@ /* 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 */ #ifndef USERSDIALOG_H_ #define USERSDIALOG_H_ #include "userinfo.h" #include #include class Ui_UsersDialog; class Ui_UsersWidget; class Pass; class QCloseEvent; class QKeyEvent; class QListWidgetItem; struct UsersWidgetData { - UsersWidgetData(Pass &pass) - : m_pass(pass) + explicit UsersWidgetData(Pass &pass) + : m_pass(pass), ui(nullptr) { } QList m_userList; QString m_dir; - Ui_UsersWidget *ui; Pass &m_pass; + Ui_UsersWidget *ui; void generateUserList(); void init(); void setUi(Ui_UsersWidget *ui); void itemChange(QListWidgetItem *item); void populateList(); }; /*! \class UsersDialog \brief Handles listing and editing of GPG users. Selection of whom to encrypt to. */ class UsersDialog : public QDialog { Q_OBJECT public: - explicit UsersDialog(QString dir, Pass &pass, QWidget *parent = nullptr); + explicit UsersDialog(const QString& dir, Pass &pass, QWidget *parent = nullptr); ~UsersDialog(); public Q_SLOTS: void accept() override; private: std::unique_ptr dialogUi; std::unique_ptr ui; UsersWidgetData d; }; #endif // USERSDIALOG_H_