diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..eafba84 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,27 @@ +--- +# clang-analyzer-cplusplus.NewDeleteLeaks does not really work with QObject ownership +# readability-magic-numbers is duplicate of cppcoreguidelines-avoid-magic-numbers +# cppcoreguildelines-pro-type-vararg and hicpp-vararg are disabled as they are confused by +# qCDebug/qCWarning syntax + +Checks: -*, + bugprone-*, + clang-analyzer-*, + -clang-analyzer-cplusplus.NewDeleteLeaks, + cppcoreguidelines-*, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-type-vararg, + google-*, + -google-readability-namespace-comments, + -google-runtime-references, + -google-build-using-namespace, + hicpp-*, + -hicpp-vararg, + misc-*, + -misc-non-private-member-variables-in-classes, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + readability-*, + -readability-magic-numbers diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e3de51..407f6db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,50 +1,53 @@ cmake_minimum_required(VERSION 3.5) set(PROJECT_VERSION "1.1.0") project(plasma-plasmapass VERSION ${PROJECT_VERSION}) set(PROJECT_VERSION_MAJOR 1) set(QT_MIN_VERSION "5.11") set(KF5_MIN_VERSION "5.57.0") set(CMAKE_CXX_STANDARD 14) find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Plasma I18n ItemModels ) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core DBus Gui Qml Concurrent ) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DQT_NO_FOREACH) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -pedantic") +set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -pedantic") + # plasmoid plasma_install_package(package org.kde.plasma.pass) # qml extension plugin add_subdirectory(plugin) if (BUILD_TESTING) add_subdirectory(tests) endif() install( FILES plasma-pass.categories DESTINATION ${KDE_INSTALL_CONFDIR} ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/plugin/abbreviations.cpp b/plugin/abbreviations.cpp index 0fe5f9c..7f00674 100644 --- a/plugin/abbreviations.cpp +++ b/plugin/abbreviations.cpp @@ -1,200 +1,208 @@ /* * Borrowed from KDevelop (kdevplatform/language/interfaces/abbreviations.cpp) * * Copyright 2014 Sven Brauch * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "abbreviations.h" #include #include namespace { +constexpr const std::size_t offsetsSize = 32; +constexpr const int maxDepth = 128; + // Taken and adapted for kdevelop from katecompletionmodel.cpp -static bool matchesAbbreviationHelper(const QStringRef &word, const QStringRef &typed, - const QVarLengthArray< int, 32 > &offsets, - int &depth, int atWord = -1, int i = 0) +bool matchesAbbreviationHelper(const QStringRef &word, const QStringRef &typed, + const QVarLengthArray &offsets, + int &depth, int atWord = -1, int i = 0) { int atLetter = 1; for ( ; i < typed.size(); i++ ) { const QChar c = typed.at(i).toLower(); bool haveNextWord = offsets.size() > atWord + 1; bool canCompare = atWord != -1 && word.size() > offsets.at(atWord) + atLetter; if (canCompare && c == word.at(offsets.at(atWord) + atLetter).toLower()) { // the typed letter matches a letter after the current word beginning if (!haveNextWord || c != word.at(offsets.at(atWord + 1)).toLower()) { // good, simple case, no conflict atLetter += 1; continue; } // For maliciously crafted data, the code used here theoretically can have very high // complexity. Thus ensure we don't run into this case, by limiting the amount of branches // we walk through to 128. depth++; - if (depth > 128) { + if (depth > maxDepth) { return false; } // the letter matches both the next word beginning and the next character in the word if (haveNextWord && matchesAbbreviationHelper(word, typed, offsets, depth, atWord + 1, i + 1)) { // resolving the conflict by taking the next word's first character worked, fine return true; } // otherwise, continue by taking the next letter in the current word. atLetter += 1; continue; - } else if (haveNextWord && c == word.at(offsets.at(atWord + 1)).toLower()) { + } + + if (haveNextWord && c == word.at(offsets.at(atWord + 1)).toLower()) { // the typed letter matches the next word beginning atWord++; atLetter = 1; continue; } + // no match return false; } // all characters of the typed word were matched return true; } } bool PlasmaPass::matchesAbbreviation(const QStringRef &word, const QStringRef &typed) { // A mismatch is very likely for random even for the first letter, // thus this optimization makes sense. if (word.at(0).toLower() != typed.at(0).toLower()) { return false; } // First, check if all letters are contained in the word in the right order. int atLetter = 0; for (const auto c : typed) { while (c.toLower() != word.at(atLetter).toLower()) { atLetter += 1; if (atLetter >= word.size()) { return false; } } } bool haveUnderscore = true; - QVarLengthArray offsets; + QVarLengthArray offsets; // We want to make "KComplM" match "KateCompletionModel"; this means we need // to allow parts of the typed text to be not part of the actual abbreviation, // which consists only of the uppercased / underscored letters (so "KCM" in this case). // However it might be ambigous whether a letter is part of such a word or part of // the following abbreviation, so we need to find all possible word offsets first, // then compare. for (int i = 0; i < word.size(); ++i) { const QChar c = word.at(i); if (c == QLatin1Char('_') || c == QLatin1Char('-')) { haveUnderscore = true; } else if (haveUnderscore || c.isUpper()) { offsets.append(i); haveUnderscore = false; } } int depth = 0; return matchesAbbreviationHelper(word, typed, offsets, depth); } bool PlasmaPass::matchesPath(const QStringRef &path, const QStringRef &typed) { int consumed = 0; int pos = 0; // try to find all the characters in typed in the right order in the path; // jumps are allowed everywhere while (consumed < typed.size() && pos < path.size()) { if (typed.at(consumed).toLower() == path.at(pos).toLower()) { consumed++; } pos++; } return consumed == typed.size(); } int PlasmaPass::matchPathFilter(const QVector &toFilter, const QVector &text) { enum PathFilterMatchQuality { NoMatch = -1, ExactMatch = 0, StartMatch = 1, OtherMatch = 2 // and anything higher than that }; - const auto segments = toFilter; + const auto &segments = toFilter; if (text.count() > segments.count()) { // number of segments mismatches, thus item cannot match return NoMatch; } bool allMatched = true; int searchIndex = text.size() - 1; int pathIndex = segments.size() - 1; int lastMatchIndex = -1; // stop early if more search fragments remain than available after path index while (pathIndex >= 0 && searchIndex >= 0 && (pathIndex + text.size() - searchIndex - 1) < segments.size()) { const auto &segment = segments.at(pathIndex); const auto &typedSegment = text.at(searchIndex); const int matchIndex = segment.indexOf(typedSegment, 0, Qt::CaseInsensitive); const bool isLastPathSegment = pathIndex == segments.size() - 1; const bool isLastSearchSegment = searchIndex == text.size() - 1; // check for exact matches allMatched &= matchIndex == 0 && segment.size() == typedSegment.size(); // check for fuzzy matches bool isMatch = matchIndex != -1; // do fuzzy path matching on the last segment if (!isMatch && isLastPathSegment && isLastSearchSegment) { isMatch = matchesPath(segment, typedSegment); } else if (!isMatch) { // check other segments for abbreviations isMatch = matchesAbbreviation(segment.mid(0), typedSegment); } if (!isMatch) { // no match, try with next path segment --pathIndex; continue; } // else we matched if (isLastPathSegment) { lastMatchIndex = matchIndex; } --searchIndex; --pathIndex; } if (searchIndex != -1) { return NoMatch; } const int segmentMatchDistance = segments.size() - (pathIndex + 1); if (allMatched) { return ExactMatch; - } else if (lastMatchIndex == 0) { + } + + if (lastMatchIndex == 0) { // prefer matches whose last element starts with the filter return StartMatch; - } else { - // prefer matches closer to the end of the path - return OtherMatch + segmentMatchDistance; } + + // prefer matches closer to the end of the path + return OtherMatch + segmentMatchDistance; } diff --git a/plugin/passwordfiltermodel.cpp b/plugin/passwordfiltermodel.cpp index 4d21cb8..e815797 100644 --- a/plugin/passwordfiltermodel.cpp +++ b/plugin/passwordfiltermodel.cpp @@ -1,268 +1,268 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "passwordfiltermodel.h" #include "passwordsmodel.h" #include "abbreviations.h" #include #include #include #include #include #include using namespace PlasmaPass; namespace { -constexpr static const auto invalidateDelay = std::chrono::milliseconds(100); -constexpr static const char newFilterProperty[] = "newFilter"; +constexpr const auto invalidateDelay = std::chrono::milliseconds(100); +constexpr const char *newFilterProperty = "newFilter"; class ModelIterator { public: using reference = const QModelIndex &; using pointer = QModelIndex *; using value_type = QModelIndex; using difference_type = int; using iterator_category = std::forward_iterator_tag; static ModelIterator begin(QAbstractItemModel *model) { - return ModelIterator(model, model->index(0, 0)); + return ModelIterator{model, model->index(0, 0)}; } static ModelIterator end(QAbstractItemModel *model) { - return ModelIterator(model, {}); + return ModelIterator{model, {}}; } bool operator==(const ModelIterator &other) const { return mModel == other.mModel && mIndex == other.mIndex; } bool operator!=(const ModelIterator &other) const { return !(*this == other); } QModelIndex operator*() const { return mIndex; } const QModelIndex *operator->() const { return &mIndex; } ModelIterator &operator++() { if (mIndex.row() < mModel->rowCount() - 1) { mIndex = mModel->index(mIndex.row() + 1, mIndex.column()); } else { mIndex = {}; } return *this; } ModelIterator operator++(int) { ModelIterator it = *this; ++*this; return it; } private: ModelIterator(QAbstractItemModel *model, const QModelIndex &index) : mModel(model), mIndex(index) {} private: QAbstractItemModel *mModel = nullptr; QModelIndex mIndex; }; } // namespace -PasswordFilterModel::PathFilter::PathFilter(const QString &filter) - : filter(filter) +PasswordFilterModel::PathFilter::PathFilter(QString filter) + : filter(std::move(filter)) {} PasswordFilterModel::PathFilter::PathFilter(const PathFilter &other) : filter(other.filter) { updateParts(); } PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(const PathFilter &other) { filter = other.filter; updateParts(); return *this; } PasswordFilterModel::PathFilter::PathFilter(PathFilter &&other) noexcept : filter(std::move(other.filter)) { updateParts(); } PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(PathFilter &&other) noexcept { filter = std::move(other.filter); updateParts(); return *this; } void PasswordFilterModel::PathFilter::updateParts() { mParts = filter.splitRef(QLatin1Char('/'), QString::SkipEmptyParts); } PasswordFilterModel::PathFilter::result_type PasswordFilterModel::PathFilter::operator()(const QModelIndex &index) const { const auto path = index.model()->data(index, PasswordsModel::FullNameRole).toString(); const auto weight = matchPathFilter(path.splitRef(QLatin1Char('/')), mParts); return std::make_pair(index, weight); } PasswordFilterModel::PasswordFilterModel(QObject *parent) : QSortFilterProxyModel(parent) , mFlatModel(new KDescendantsProxyModel(this)) { mFlatModel->setDisplayAncestorData(false); sort(0); // enable sorting mUpdateTimer.setSingleShot(true); connect(&mUpdateTimer, &QTimer::timeout, this, &PasswordFilterModel::delayedUpdateFilter); connect(&mUpdateTimer, &QTimer::timeout, this, []() { qDebug() << "Update timer timeout, will calculate results lazily."; }); } void PasswordFilterModel::setSourceModel(QAbstractItemModel *sourceModel) { mFlatModel->setSourceModel(sourceModel); - if (!this->sourceModel()) { + if (this->sourceModel() == nullptr) { QSortFilterProxyModel::setSourceModel(mFlatModel); } } QString PasswordFilterModel::passwordFilter() const { return mFilter.filter; } void PasswordFilterModel::setPasswordFilter(const QString &filter) { if (mFilter.filter != filter) { if (mUpdateTimer.isActive()) { mUpdateTimer.stop(); } mUpdateTimer.setProperty(newFilterProperty, filter); mUpdateTimer.start(invalidateDelay); if (mFuture.isRunning()) { mFuture.cancel(); } if (!filter.isEmpty()) { mFuture = QtConcurrent::mappedReduced>( ModelIterator::begin(sourceModel()), ModelIterator::end(sourceModel()), PathFilter{filter}, [](QHash &result, const std::pair &value) { result.insert(value.first, value.second); }); auto watcher = new QFutureWatcher>(); connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher]() { mSortingLookup = mFuture.result(); watcher->deleteLater(); // If the timer is not active it means we were to slow, so don't invoke // delayedUpdateFilter() again, just update mSortingLookup with our // results. if (mUpdateTimer.isActive()) { mUpdateTimer.stop(); delayedUpdateFilter(); } }); connect(watcher, &QFutureWatcherBase::canceled, watcher, &QObject::deleteLater); watcher->setFuture(mFuture); } } } void PasswordFilterModel::delayedUpdateFilter() { mFilter = PathFilter(mUpdateTimer.property(newFilterProperty).toString()); Q_EMIT passwordFilterChanged(); if (mFuture.isRunning()) { // If the future is still running, clear up the lookup table, we will have to // calculate the intermediate results ourselves mSortingLookup.clear(); } invalidate(); } QVariant PasswordFilterModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { return data(index, PasswordsModel::FullNameRole); } return QSortFilterProxyModel::data(index, role); } bool PasswordFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { const auto src_index = sourceModel()->index(source_row, 0, source_parent); const auto type = static_cast(sourceModel()->data(src_index, PasswordsModel::EntryTypeRole).toInt()); if (type == PasswordsModel::FolderEntry) { return false; } if (mFilter.filter.isEmpty()) { return true; } // Try to lookup the weight in the lookup table, the worker thread may have put it in there // while the updateTimer was ticking auto weight = mSortingLookup.find(src_index); if (weight == mSortingLookup.end()) { // It's not there, let's calculate the value now const auto result = mFilter(src_index); weight = mSortingLookup.insert(result.first, result.second); } return *weight > -1; } bool PasswordFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { const auto weightLeft = mSortingLookup.value(source_left, -1); const auto weightRight = mSortingLookup.value(source_right, -1); if (weightLeft == weightRight) { const auto nameLeft = source_left.data(PasswordsModel::FullNameRole).toString(); const auto nameRight = source_right.data(PasswordsModel::FullNameRole).toString(); return QString::localeAwareCompare(nameLeft, nameRight) < 0; } return weightLeft < weightRight; } diff --git a/plugin/passwordfiltermodel.h b/plugin/passwordfiltermodel.h index 56998e6..0b8889d 100644 --- a/plugin/passwordfiltermodel.h +++ b/plugin/passwordfiltermodel.h @@ -1,86 +1,86 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PASSWORDFILTERMODEL_H_ #define PASSWORDFILTERMODEL_H_ #include #include #include #include class QStringRef; class KDescendantsProxyModel; namespace PlasmaPass { class PasswordFilterModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QString passwordFilter READ passwordFilter WRITE setPasswordFilter NOTIFY passwordFilterChanged) public: explicit PasswordFilterModel(QObject *parent = nullptr); void setSourceModel(QAbstractItemModel *sourceModel) override; QString passwordFilter() const; void setPasswordFilter(const QString &filter); QVariant data(const QModelIndex &index, int role) const override; Q_SIGNALS: void passwordFilterChanged(); protected: bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; private: struct PathFilter { using result_type = std::pair; explicit PathFilter() = default; - PathFilter(const QString &filter); + PathFilter(QString filter); PathFilter(const PathFilter &); PathFilter(PathFilter &&) noexcept; PathFilter &operator=(const PathFilter &); PathFilter &operator=(PathFilter &&) noexcept; result_type operator()(const QModelIndex &index) const; QString filter; private: void updateParts(); QVector mParts; }; void delayedUpdateFilter(); KDescendantsProxyModel *mFlatModel = nullptr; PathFilter mFilter; mutable QHash mSortingLookup; QTimer mUpdateTimer; QFuture> mFuture; }; } #endif diff --git a/plugin/passwordprovider.cpp b/plugin/passwordprovider.cpp index b974a6a..57d5c5f 100644 --- a/plugin/passwordprovider.cpp +++ b/plugin/passwordprovider.cpp @@ -1,279 +1,276 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "passwordprovider.h" #include "klipperinterface.h" #include "plasmapass_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono; using namespace std::chrono_literals; namespace { -static const auto PasswordTimeout = 45s; -static const auto PasswordTimeoutUpdateInterval = 100ms; +constexpr const auto PasswordTimeout = 45s; +constexpr const auto PasswordTimeoutUpdateInterval = 100ms; -} +const QString klipperDBusService = QStringLiteral("org.kde.klipper"); +const QString klipperDBusPath = QStringLiteral("/klipper"); +const QString klipperDataEngine = QStringLiteral("org.kde.plasma.clipboard"); -#define KLIPPER_DBUS_SERVICE QStringLiteral("org.kde.klipper") -#define KLIPPER_DBUS_PATH QStringLiteral("/klipper") -#define KLIPPER_DATA_ENGINE QStringLiteral("org.kde.plasma.clipboard") +} using namespace PlasmaPass; PasswordProvider::PasswordProvider(const QString &path, QObject *parent) : QObject(parent) { mTimer.setInterval(duration_cast(PasswordTimeoutUpdateInterval).count()); connect(&mTimer, &QTimer::timeout, this, [this]() { mTimeout -= mTimer.interval(); Q_EMIT timeoutChanged(); if (mTimeout == 0) { expirePassword(); } }); bool isGpg2 = true; auto gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2")); if (gpgExe.isEmpty()) { gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg")); isGpg2 = false; } if (gpgExe.isEmpty()) { qCWarning(PLASMAPASS_LOG, "Failed to find gpg or gpg2 executables"); setError(i18n("Failed to decrypt password: GPG is not available")); return; } QStringList args = { QStringLiteral("-d"), QStringLiteral("--quiet"), QStringLiteral("--yes"), QStringLiteral("--compress-algo=none"), QStringLiteral("--no-encrypt-to"), path }; if (isGpg2) { args = QStringList{ QStringLiteral("--batch"), QStringLiteral("--use-agent") } + args; } - mGpg = new QProcess; + mGpg = std::make_unique(); // Let's not be like animals and deal with this asynchronously - connect(mGpg, &QProcess::errorOccurred, + connect(mGpg.get(), &QProcess::errorOccurred, this, [this, gpgExe](QProcess::ProcessError state) { if (state == QProcess::FailedToStart) { qCWarning(PLASMAPASS_LOG, "Failed to start %s: %s", qUtf8Printable(gpgExe), qUtf8Printable(mGpg->errorString())); setError(i18n("Failed to decrypt password: Failed to start GPG")); } }); - connect(mGpg, &QProcess::readyReadStandardOutput, + connect(mGpg.get(), &QProcess::readyReadStandardOutput, this, [this]() { // We only read the first line, second line usually the username setPassword(QString::fromUtf8(mGpg->readLine()).trimmed()); }); - connect(mGpg, QOverload::of(&QProcess::finished), + connect(mGpg.get(), QOverload::of(&QProcess::finished), this, [this]() { const auto err = mGpg->readAllStandardError(); if (mPassword.isEmpty()) { if (err.isEmpty()) { setError(i18n("Failed to decrypt password")); } else { setError(i18n("Failed to decrypt password: %1").arg(QString::fromUtf8(err))); } } - mGpg->deleteLater(); - mGpg = nullptr; + mGpg.reset(); }); mGpg->setProgram(gpgExe); mGpg->setArguments(args); mGpg->start(QIODevice::ReadOnly); } PasswordProvider::~PasswordProvider() { if (mGpg) { mGpg->terminate(); - delete mGpg; } } bool PasswordProvider::isValid() const { return !mPassword.isNull(); } QString PasswordProvider::password() const { return mPassword; } -QMimeData *PasswordProvider::mimeDataForPassword(const QString &password) const +QMimeData *PasswordProvider::mimeDataForPassword(const QString &password) { auto mimeData = new QMimeData; mimeData->setText(password); // https://phabricator.kde.org/D12539 mimeData->setData(QStringLiteral("x-kde-passwordManagerHint"), "secret"); return mimeData; } void PasswordProvider::setPassword(const QString &password) { - qGuiApp->clipboard()->setMimeData(mimeDataForPassword(password), - QClipboard::Clipboard); + auto clipboard = qGuiApp->clipboard(); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) + clipboard->setMimeData(mimeDataForPassword(password), QClipboard::Clipboard); - if (qGuiApp->clipboard()->supportsSelection()) { - qGuiApp->clipboard()->setMimeData(mimeDataForPassword(password), - QClipboard::Selection); + if (clipboard->supportsSelection()) { + clipboard->setMimeData(mimeDataForPassword(password), QClipboard::Selection); } mPassword = password; Q_EMIT validChanged(); Q_EMIT passwordChanged(); mTimeout = defaultTimeout(); Q_EMIT timeoutChanged(); mTimer.start(); } void PasswordProvider::expirePassword() { removePasswordFromClipboard(mPassword); mPassword.clear(); mTimer.stop(); Q_EMIT validChanged(); Q_EMIT passwordChanged(); // Delete the provider, it's no longer needed deleteLater(); } int PasswordProvider::timeout() const { return mTimeout; } -int PasswordProvider::defaultTimeout() const +int PasswordProvider::defaultTimeout() const // NOLINT(readability-convert-member-functions-to-static) { return duration_cast(PasswordTimeout).count(); } QString PasswordProvider::error() const { return mError; } bool PasswordProvider::hasError() const { return !mError.isNull(); } void PasswordProvider::setError(const QString &error) { mError = error; Q_EMIT errorChanged(); } void PasswordProvider::removePasswordFromClipboard(const QString &password) { // Clear the WS clipboard itself - const auto clipboard = qGuiApp->clipboard(); + const auto clipboard = qGuiApp->clipboard(); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) if (clipboard->text() == password) { clipboard->clear(); } if (!mEngineConsumer) { - mEngineConsumer = new Plasma::DataEngineConsumer(); + mEngineConsumer = std::make_unique(); } - auto engine = mEngineConsumer->dataEngine(KLIPPER_DATA_ENGINE); + auto engine = mEngineConsumer->dataEngine(klipperDataEngine); // Klipper internally identifies each history entry by it's SHA1 hash // (see klipper/historystringitem.cpp) so we try here to obtain a service directly // for the history item with our password so that we can only remove the // password from the history without having to clear the entire history. const auto service = engine->serviceForSource( QString::fromLatin1( QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha1).toBase64())); - if (!service) { + if (service == nullptr) { qCWarning(PLASMAPASS_LOG, "Failed to obtain PlasmaService for the password, falling back to clearClipboard()"); - delete std::exchange(mEngineConsumer, nullptr); + mEngineConsumer.reset(); clearClipboard(); return; } auto job = service->startOperationCall(service->operationDescription(QStringLiteral("remove"))); // FIXME: KJob::result() is an overloaded QPrivateSignal and cannot be QOverload()ed, // so we have to do it the old-school way connect(job, &KJob::result, this, &PasswordProvider::onPlasmaServiceRemovePasswordResult); } void PasswordProvider::onPlasmaServiceRemovePasswordResult(KJob* job) { // Disconnect from the job: Klipper's ClipboardJob is buggy and emits result() twice disconnect(job, &KJob::result, this, &PasswordProvider::onPlasmaServiceRemovePasswordResult); - QTimer::singleShot(0, this, [this]() { delete std::exchange(mEngineConsumer, nullptr); }); + QTimer::singleShot(0, this, [this]() { mEngineConsumer.reset(); }); auto serviceJob = qobject_cast(job); - if (serviceJob->error()) { + if (serviceJob->error() != 0) { qCWarning(PLASMAPASS_LOG, "ServiceJob for clipboard failed: %s", qUtf8Printable(serviceJob->errorString())); clearClipboard(); return; } // If something went wrong fallback to clearing the entire clipboard if (!serviceJob->result().toBool()) { qCWarning(PLASMAPASS_LOG, "ServiceJob for clipboard failed internally, falling back to clearClipboard()"); clearClipboard(); return; } qCDebug(PLASMAPASS_LOG, "Successfuly removed password from Klipper"); } void PasswordProvider::clearClipboard() { - org::kde::klipper::klipper klipper(KLIPPER_DBUS_SERVICE, KLIPPER_DBUS_PATH, QDBusConnection::sessionBus()); + org::kde::klipper::klipper klipper(klipperDBusService, klipperDBusPath, QDBusConnection::sessionBus()); if (!klipper.isValid()) { return; } klipper.clearClipboardHistory(); klipper.clearClipboardContents(); } diff --git a/plugin/passwordprovider.h b/plugin/passwordprovider.h index 7e3fec8..c925c6d 100644 --- a/plugin/passwordprovider.h +++ b/plugin/passwordprovider.h @@ -1,91 +1,93 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PASSWORDPROVIDER_H_ #define PASSWORDPROVIDER_H_ #include #include +#include + class QProcess; class QDBusPendingCallWatcher; class KJob; class QMimeData; namespace Plasma { class DataEngineConsumer; } namespace PlasmaPass { class PasswordsModel; class PasswordProvider : public QObject { Q_OBJECT Q_PROPERTY(QString password READ password NOTIFY passwordChanged) Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) Q_PROPERTY(int timeout READ timeout NOTIFY timeoutChanged) Q_PROPERTY(int defaultTimeout READ defaultTimeout CONSTANT) Q_PROPERTY(bool hasError READ hasError NOTIFY errorChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged) public: ~PasswordProvider() override; QString password() const; bool isValid() const; int timeout() const; int defaultTimeout() const; bool hasError() const; QString error() const; Q_SIGNALS: void passwordChanged(); void validChanged(); void timeoutChanged(); void errorChanged(); private Q_SLOTS: void onPlasmaServiceRemovePasswordResult(KJob *job); private: void setError(const QString &error); void setPassword(const QString &password); void expirePassword(); void removePasswordFromClipboard(const QString &password); - void clearClipboard(); + static void clearClipboard(); - QMimeData *mimeDataForPassword(const QString &password) const; + static QMimeData *mimeDataForPassword(const QString &password); friend class PasswordsModel; explicit PasswordProvider(const QString &path, QObject *parent = nullptr); - Plasma::DataEngineConsumer *mEngineConsumer = nullptr; - QProcess *mGpg = nullptr; + std::unique_ptr mEngineConsumer; + std::unique_ptr mGpg; QString mPath; QString mPassword; QString mError; QTimer mTimer; int mTimeout = 0; }; } #endif diff --git a/plugin/passwordsmodel.cpp b/plugin/passwordsmodel.cpp index ca0c9a3..3e674bd 100644 --- a/plugin/passwordsmodel.cpp +++ b/plugin/passwordsmodel.cpp @@ -1,224 +1,221 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "passwordsmodel.h" #include "passwordprovider.h" #include #include #include using namespace PlasmaPass; -#define PASSWORD_STORE_DIR "PASSWORD_STORE_DIR" +static constexpr const char *passwordStoreDir = "PASSWORD_STORE_DIR"; -class PasswordsModel::Node +struct PasswordsModel::Node { -public: - Node() {} - Node(const QString &name, PasswordsModel::EntryType type, Node *nodeParent) - : name(name), type(type), parent(nodeParent) + explicit Node() = default; + Node(QString name, PasswordsModel::EntryType type, Node *nodeParent) + : name(std::move(name)), type(type), parent(nodeParent) { - if (parent) { - parent->children.append(this); + if (parent != nullptr) { + parent->children.push_back(std::unique_ptr(this)); } } Node(const Node &other) = delete; Node(Node &&other) = default; Node &operator=(const Node &other) = delete; Node &operator=(Node &&other) = delete; - ~Node() - { - qDeleteAll(children); - } + ~Node() = default; QString path() const { - if (!parent) { + if (parent == nullptr) { return name; - } else { - QString fileName = name; - if (type == PasswordsModel::PasswordEntry) { - fileName += QStringLiteral(".gpg"); - } - return parent->path() + QLatin1Char('/') + fileName; } + + QString fileName = name; + if (type == PasswordsModel::PasswordEntry) { + fileName += QStringLiteral(".gpg"); + } + return parent->path() + QLatin1Char('/') + fileName; } QString fullName() const { if (!mFullName.isNull()) { return mFullName; } - if (!parent) { + if (parent == nullptr) { return {}; } const auto p = parent->fullName(); if (p.isEmpty()) { mFullName = name; } else { mFullName = p + QLatin1Char('/') + name; } return mFullName; } QString name; - PasswordsModel::EntryType type; + PasswordsModel::EntryType type = PasswordsModel::FolderEntry; QPointer provider; Node *parent = nullptr; - QVector children; + std::vector> children; private: mutable QString mFullName; }; PasswordsModel::PasswordsModel(QObject *parent) : QAbstractItemModel(parent) , mWatcher(this) { - if (qEnvironmentVariableIsSet(PASSWORD_STORE_DIR)) { - mPassStore = QDir(QString::fromUtf8(qgetenv(PASSWORD_STORE_DIR))); + if (qEnvironmentVariableIsSet(passwordStoreDir)) { + mPassStore = QDir(QString::fromUtf8(qgetenv(passwordStoreDir))); } else { mPassStore = QDir(QStringLiteral("%1/.password-store").arg(QDir::homePath())); } // FIXME: Try to figure out what has actually changed and update the model // accordingly instead of reseting it connect(&mWatcher, &QFileSystemWatcher::directoryChanged, this, &PasswordsModel::populate); populate(); } -PasswordsModel::~PasswordsModel() -{ - delete mRoot; -} +PasswordsModel::~PasswordsModel() = default; -PasswordsModel::Node *PasswordsModel::node(const QModelIndex& index) const +PasswordsModel::Node *PasswordsModel::node(const QModelIndex& index) { return static_cast(index.internalPointer()); } QHash PasswordsModel::roleNames() const { return { { NameRole, "name" }, { EntryTypeRole, "type" }, { FullNameRole, "fullName" }, { PathRole, "path" }, { HasPasswordRole, "hasPassword" }, { PasswordRole, "password" } }; } int PasswordsModel::rowCount(const QModelIndex &parent) const { - const auto parentNode = parent.isValid() ? node(parent) : mRoot; - return parentNode ? parentNode->children.count() : 0; + const auto parentNode = parent.isValid() ? node(parent) : mRoot.get(); + return parentNode != nullptr ? static_cast(parentNode->children.size()) : 0; } int PasswordsModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } QModelIndex PasswordsModel::index(int row, int column, const QModelIndex &parent) const { - const auto parentNode = parent.isValid() ? node(parent) : mRoot; - if (!parentNode || row < 0 || row >= parentNode->children.count() || column != 0) { + const auto parentNode = parent.isValid() ? node(parent) : mRoot.get(); + if (parentNode == nullptr || row < 0 || static_cast(row) >= parentNode->children.size() || column != 0) { return {}; } - return createIndex(row, column, parentNode->children.at(row)); + return createIndex(row, column, parentNode->children.at(row).get()); } QModelIndex PasswordsModel::parent(const QModelIndex &child) const { if (!child.isValid()) { return {}; } const auto childNode = node(child); - if (!childNode || !childNode->parent) { + if (childNode == nullptr || childNode->parent == nullptr) { return {}; } const auto parentNode = childNode->parent; - if (parentNode == mRoot) { + if (parentNode == mRoot.get()) { return {}; } - return createIndex(parentNode->parent->children.indexOf(parentNode), 0, parentNode); + + auto &children = parentNode->parent->children; + const auto it = std::find_if(children.cbegin(), children.cend(), + [parentNode](const auto &node) { return node.get() == parentNode; }); + Q_ASSERT(it != children.cend()); + return createIndex(std::distance(children.cbegin(), it), 0, parentNode); } QVariant PasswordsModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } const auto node = this->node(index); - if (!node) { + if (node == nullptr) { return {}; } switch (role) { case Qt::DisplayRole: return node->name; case EntryTypeRole: return node->type; case PathRole: return node->path(); case FullNameRole: return node->fullName(); case PasswordRole: - if (!node->provider) { + if (node->provider == nullptr) { node->provider = new PasswordProvider(node->path()); } return QVariant::fromValue(node->provider.data()); case HasPasswordRole: return !node->provider.isNull(); + default: + return {}; } - - return {}; } void PasswordsModel::populate() { beginResetModel(); - delete mRoot; - mRoot = new Node; + mRoot = std::make_unique(); mRoot->name = mPassStore.absolutePath(); - populateDir(mPassStore, mRoot); + populateDir(mPassStore, mRoot.get()); endResetModel(); } void PasswordsModel::populateDir(const QDir& dir, Node *parent) { mWatcher.addPath(dir.absolutePath()); auto entries = dir.entryInfoList({ QStringLiteral("*.gpg") }, QDir::Files, QDir::NoSort); for (const auto &entry : qAsConst(entries)) { new Node(entry.completeBaseName(), PasswordEntry, parent); } entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort); for (const auto &entry : qAsConst(entries)) { auto node = new Node(entry.fileName(), FolderEntry, parent); populateDir(entry.absoluteFilePath(), node); } } diff --git a/plugin/passwordsmodel.h b/plugin/passwordsmodel.h index 394e80b..e0d5f4e 100644 --- a/plugin/passwordsmodel.h +++ b/plugin/passwordsmodel.h @@ -1,76 +1,78 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef PASSWORDSMODEL_H_ #define PASSWORDSMODEL_H_ #include #include #include +#include + namespace PlasmaPass { class PasswordsModel : public QAbstractItemModel { Q_OBJECT - class Node; + struct Node; public: enum EntryType { FolderEntry, PasswordEntry }; Q_ENUM(EntryType) enum Roles { NameRole = Qt::DisplayRole, EntryTypeRole = Qt::UserRole, FullNameRole, PathRole, PasswordRole, HasPasswordRole, }; explicit PasswordsModel(QObject *parent = nullptr); ~PasswordsModel() override; QHash roleNames() const override; int rowCount(const QModelIndex & parent) const override; int columnCount(const QModelIndex & parent) const override; QModelIndex index(int row, int column, const QModelIndex & parent) const override; QModelIndex parent(const QModelIndex & child) const override; QVariant data(const QModelIndex &index, int role) const override; private: void populate(); void populateDir(const QDir &dir, Node *parent); - Node *node(const QModelIndex &index) const; + static Node *node(const QModelIndex &index); QFileSystemWatcher mWatcher; QDir mPassStore; - Node *mRoot = nullptr; + std::unique_ptr mRoot; }; } #endif diff --git a/tests/passwordsmodeltest/main.cpp b/tests/passwordsmodeltest/main.cpp index 0b9589c..e5e0bb9 100644 --- a/tests/passwordsmodeltest/main.cpp +++ b/tests/passwordsmodeltest/main.cpp @@ -1,31 +1,31 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "mainwindow.h" int main(int argc, char **argv) { QApplication app(argc, argv); MainWindow window; window.showNormal(); - return app.exec(); + return QApplication::exec(); } diff --git a/tests/passwordsmodeltest/mainwindow.cpp b/tests/passwordsmodeltest/mainwindow.cpp index 698204f..3eca581 100644 --- a/tests/passwordsmodeltest/mainwindow.cpp +++ b/tests/passwordsmodeltest/mainwindow.cpp @@ -1,179 +1,177 @@ /* * Copyright (C) 2018 Daniel Vrátil * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mainwindow.h" #include "passwordsmodel.h" #include "passwordprovider.h" #include "passwordfiltermodel.h" #include #include #include #include #include #include #include #include #include #include using namespace PlasmaPass; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { - resize(900, 350); + resize(900, 350); // NOLINT(cppcoreguidelines-avoid-magic-numbers) - QWidget *w = new QWidget; + auto w = new QWidget; setCentralWidget(w); auto h = new QHBoxLayout(w); auto splitter = new QSplitter; h->addWidget(splitter); w = new QWidget; splitter->addWidget(w); auto v = new QVBoxLayout(w); auto input = new QLineEdit; input->setClearButtonEnabled(true); input->setPlaceholderText(QStringLiteral("Search ...")); connect(input, &QLineEdit::textChanged, this, &MainWindow::onSearchChanged); v->addWidget(input); mStack = new QStackedWidget; v->addWidget(mStack); auto treeView = new QTreeView; treeView->setHeaderHidden(true); treeView->setModel(new PasswordsModel(this)); connect(treeView, &QTreeView::clicked, this, &MainWindow::onPasswordClicked); mStack->addWidget(treeView); auto listView = new QListView; mFilterModel = new PasswordFilterModel(listView); mFilterModel->setSourceModel(treeView->model()); listView->setModel(mFilterModel); connect(listView, &QListView::clicked, this, &MainWindow::onPasswordClicked); mStack->addWidget(listView); mStack->setCurrentIndex(0); w = new QWidget; splitter->addWidget(w); v = new QVBoxLayout(w); v->addWidget(mTitle = new QLabel); auto font = mTitle->font(); font.setBold(true); mTitle->setFont(font); auto g = new QFormLayout; v->addLayout(g); g->addRow(QStringLiteral("Path:"), mPath = new QLabel()); g->addRow(QStringLiteral("Type:"), mType = new QLabel()); g->addRow(QStringLiteral("Password:"), mPassword = new QLabel()); g->addRow(QStringLiteral("Expiration:"), mPassProgress = new QProgressBar()); g->addRow(QStringLiteral("Error:"), mError = new QLabel()); mPassProgress->setTextVisible(false); v->addWidget(mPassBtn = new QPushButton(QStringLiteral("Display Password"))); connect(mPassBtn, &QPushButton::clicked, this, [this]() { setProvider(mCurrent.data(PasswordsModel::PasswordRole).value()); }); - v->addStretch(2.0); + v->addStretch(2.0); // NOLINT(cppcoreguidelines-avoid-magic-numbers) onPasswordClicked({}); } -MainWindow::~MainWindow() -{ -} +MainWindow::~MainWindow() = default; void MainWindow::setProvider(PasswordProvider *provider) { mProvider = provider; if (provider->isValid()) { mPassBtn->setVisible(false); mPassword->setVisible(true); mPassword->setText(provider->password()); } if (provider->hasError()) { mError->setVisible(true); mError->setText(provider->error()); } connect(provider, &PasswordProvider::passwordChanged, this, [this, provider]() { const auto pass = provider->password(); if (!pass.isEmpty()) { mPassword->setVisible(true); mPassword->setText(provider->password()); } else { onPasswordClicked(mCurrent); } }); connect(provider, &PasswordProvider::timeoutChanged, this, [this, provider]() { mPassProgress->setVisible(true); mPassProgress->setMaximum(provider->defaultTimeout()); mPassProgress->setValue(provider->timeout()); }); connect(provider, &PasswordProvider::errorChanged, this, [this, provider]() { mError->setVisible(true); mError->setText(provider->error()); }); } void MainWindow::onPasswordClicked(const QModelIndex &idx) { - if (mProvider) { + if (mProvider != nullptr) { mProvider->disconnect(this); } mCurrent = idx; mTitle->setText(idx.data(PasswordsModel::NameRole).toString()); mPath->setText(idx.data(PasswordsModel::PathRole).toString()); const auto type = idx.isValid() ? static_cast(idx.data(PasswordsModel::EntryTypeRole).toInt()) : PasswordsModel::FolderEntry; mType->setText(type == PasswordsModel::PasswordEntry ? QStringLiteral("Password") : QStringLiteral("Folder")); mPassword->setVisible(false); mPassword->clear(); mPassBtn->setEnabled(type == PasswordsModel::PasswordEntry); mPassBtn->setVisible(true); mPassProgress->setVisible(false); mError->clear(); mError->setVisible(false); const auto hasProvider = mCurrent.data(PasswordsModel::HasPasswordRole).toBool(); if (hasProvider) { setProvider(mCurrent.data(PasswordsModel::PasswordRole).value()); } } void MainWindow::onSearchChanged(const QString &text) { mStack->setCurrentIndex(text.isEmpty() ? 0 : 1); mFilterModel->setPasswordFilter(text); }