From 2409f33f5959a24dadfe0e0b086522c61a560398 Mon Sep 17 00:00:00 2001 From: Quentin Rouland Date: Wed, 5 Feb 2025 11:02:59 +0100 Subject: [PATCH] Add search password feature (equivalent to pass find) --- plugins/Pass/pass.cpp | 32 +++++- plugins/Pass/pass.h | 11 ++ qml/components/FileDir.qml | 68 ++++++++---- qml/pages/PasswordList.qml | 101 +++++++++++++++--- qml/pages/headers/MainHeader.qml | 21 ++-- tests.in.cpp | 2 +- tests/plugins/TestsUtils/CMakeLists.txt | 25 ----- tests/plugins/TestsUtils/passphraseprovider.h | 6 +- tests/plugins/TestsUtils/utils.cpp | 14 +-- tests/plugins/TestsUtils/utils.h | 4 +- tests/units/pass/PassTestCase.qml | 3 +- tests/units/pass/tst_ls.qml | 44 ++++++++ tests/units/pass/tst_show.qml | 8 +- 13 files changed, 250 insertions(+), 89 deletions(-) create mode 100644 tests/units/pass/tst_ls.qml diff --git a/plugins/Pass/pass.cpp b/plugins/Pass/pass.cpp index d9df785..32cf0bc 100644 --- a/plugins/Pass/pass.cpp +++ b/plugins/Pass/pass.cpp @@ -1,7 +1,8 @@ #include #include #include - +#include +#include #include "jobs/decryptjob.h" #include "jobs/deletekeyjob.h" #include "jobs/getkeysjob.h" @@ -61,8 +62,35 @@ void Pass::initPasswordStore() qInfo() << "[Pass] Password Store is :" << m_password_store; } +void Pass::lsJob() +{ + QDirIterator it(this->m_password_store, QStringList() << "*.gpg", QDir::Files, QDirIterator::Subdirectories); + QList ret; + while (it.hasNext()) { + QFile f(it.next()); + QString fname = f.fileName(); + fname.remove(0, this->m_password_store.length() + 1); // remove system path + ret.append(fname); + } + qInfo() << "[Pass] ls Succeed"; + emit lsSucceed(ret); + this->m_sem->release(1); +} + +bool Pass::ls() +{ + qInfo() << "[Pass] ls"; + if (!this->m_sem->tryAcquire(1, 500)) { + qInfo() << "[Pass] A command is already running"; + return false; + } + QtConcurrent::run(this, &Pass::lsJob ); + return true; +} + bool Pass::show(QUrl url) { + qInfo() << "[Pass] Show"; if (!this->m_sem->tryAcquire(1, 500)) { qInfo() << "[Pass] A command is already running"; return false; @@ -111,7 +139,7 @@ void Pass::slotDeletePasswordStoreResult(bool err) { this->initPasswordStore(); // reinit an empty password-store if (err) { - qInfo() << "[Pass] delete Password Store Failed"; + qInfo() << "[Pass] Delete Password Store Failed"; emit deletePasswordStoreFailed("failed to delete password store"); } else { qInfo() << "[Pass] Delete Password Store Succeed"; diff --git a/plugins/Pass/pass.h b/plugins/Pass/pass.h index e7c9787..2132c41 100644 --- a/plugins/Pass/pass.h +++ b/plugins/Pass/pass.h @@ -118,6 +118,9 @@ signals: */ void showSucceed(QString name, QString text); + + void lsSucceed(QList); + /** * @brief Emitted when showing a password fails. * @param message The error message describing the failure. @@ -160,6 +163,8 @@ private: */ void initPasswordStore(); + void lsJob(); + public: /** * @brief Constructs the Pass object. @@ -229,6 +234,12 @@ public: // Password store-related methods + /** + * @brief Get the list of password. + * @return The list of password in the password store. + */ + Q_INVOKABLE bool ls(); + /** * @brief Launch the job to shows the password associated with the specified URL. * @param url The URL pointing to the password store entry. diff --git a/qml/components/FileDir.qml b/qml/components/FileDir.qml index f20361a..2cff6d9 100644 --- a/qml/components/FileDir.qml +++ b/qml/components/FileDir.qml @@ -6,15 +6,33 @@ import Lomiri.Components.Themes 1.3 import Pass 1.0 import QtQuick 2.4 -Component { +Item { + //property string folder + + id: fileDir + + property string fName + property bool fIsDir + property bool commonBorder: true + property int lBorderwidth: 0 + property int rBorderwidth: 0 + property int tBorderwidth: 0 + property int bBorderwidth: 0 + property int commonBorderWidth: 0 + property string borderColor: LomiriColors.warmGrey + + signal clicked() + + anchors.right: parent.right + anchors.left: parent.left + height: units.gu(5) + Rectangle { - anchors.right: parent.right - anchors.left: parent.left - height: units.gu(5) + anchors.fill: parent color: theme.palette.normal.background Text { - text: fileIsDir ? fileName : fileName.slice(0, -4) // remove .gpg if it's a file + text: fileDir.fIsDir ? fileDir.fName : fileDir.fName.slice(0, -4) // remove .gpg if it's a file anchors.left: parent.left anchors.leftMargin: units.gu(2) anchors.verticalCenter: parent.verticalCenter @@ -26,32 +44,36 @@ Component { anchors.verticalCenter: parent.verticalCenter anchors.rightMargin: units.gu(2) height: units.gu(4) - name: fileIsDir ? "go-next" : "lock" + name: fileDir.fIsDir ? "go-next" : "lock" color: LomiriColors.orange } MouseArea { + // onClicked: { + // var path = fileDir.fdfolder + "/" + fileName; + // if (fileIsDir) { + // fileDir.fdfolder = path; + // //backAction.visible = true; + // // passwordListHeader.title = fileName; + // } else { + // console.debug("pass show %1".arg(path)); + // Pass.show(path); + // } + // } + anchors.fill: parent - onClicked: { - var path = folderModel.folder + "/" + fileName; - if (fileIsDir) { - folderModel.folder = path; - backAction.visible = true; - passwordListHeader.title = fileName; - } else { - console.debug("pass show %1".arg(path)); - Pass.show(path); - } - } + onClicked: fileDir.clicked() } CustomBorder { - commonBorder: false - lBorderwidth: 0 - rBorderwidth: 0 - tBorderwidth: 0 - bBorderwidth: 1 - borderColor: LomiriColors.warmGrey + id: cb + + commonBorder: fileDir.commonBorder + lBorderwidth: fileDir.lBorderwidth + rBorderwidth: fileDir.rBorderwidth + tBorderwidth: fileDir.tBorderwidth + bBorderwidth: fileDir.bBorderwidth + borderColor: fileDir.borderColor } } diff --git a/qml/pages/PasswordList.qml b/qml/pages/PasswordList.qml index 3dcc596..f39d15b 100644 --- a/qml/pages/PasswordList.qml +++ b/qml/pages/PasswordList.qml @@ -10,11 +10,36 @@ import "headers" Page { id: passwordListPage - property string passwordStorePath + property string __passwordStorePath + property var __passwords + + function __searchPasswords(filter) { + var ret = []; + if (__passwords) { + for (var i = 0; i < __passwords.length; i++) { + if (__passwords[i].toUpperCase().indexOf(filter.toUpperCase()) > -1) + ret.push(__passwords[i]); + + } + } + return ret; + } + + function __searchUpdateModel(text) { + var ret = __searchPasswords(text); + passwordListSearch.model.clear(); + for (var i = 0; i < ret.length; i++) { + if (ret[i]) + passwordListSearch.model.append({ + "fileName": ret[i] + }); + + } + } anchors.fill: parent Component.onCompleted: { - passwordStorePath = "file:" + Pass.password_store; + __passwordStorePath = "file:" + Pass.password_store; Pass.onShowSucceed.connect(function(filename, text) { pageStack.push(Qt.resolvedUrl("../pages/Password.qml"), { "plainText": text, @@ -24,16 +49,22 @@ Page { Pass.onShowFailed.connect(function(message) { PopupUtils.open(passwordPageDecryptError); }); + Pass.onLsSucceed.connect(function(passwords) { + __passwords = passwords; + }); + Pass.ls(); } Column { + id: passwordListEmpty + anchors.top: passwordListHeader.bottom anchors.bottom: parent.bottom anchors.right: parent.right anchors.left: parent.left anchors.leftMargin: units.gu(2) anchors.rightMargin: units.gu(2) - visible: folderModel.count == 0 + visible: passwordListNav.model.count === 0 Rectangle { width: parent.width @@ -71,24 +102,66 @@ Page { } ListView { + id: passwordListNav + anchors.top: passwordListHeader.bottom anchors.bottom: parent.bottom anchors.right: parent.right anchors.left: parent.left spacing: 1 - visible: folderModel.count != 0 + visible: passwordListNav.model.count !== 0 && passwordListHeader.searchBarIsActive model: FolderListModel { - id: folderModel - nameFilters: ["*.gpg"] - rootFolder: passwordStorePath - folder: passwordStorePath + rootFolder: __passwordStorePath + folder: __passwordStorePath showDirs: true } - delegate: FileDir { - id: fileDelegate + delegate: Component { + FileDir { + fName: fileName + fIsDir: fileIsDir + onClicked: { + var path = passwordListNav.model.folder + "/" + fileName; + if (fileIsDir) { + passwordListNav.model.folder = path; + backAction.visible = true; + passwordListHeader.title = fileName; + } else { + console.debug("pass show %1".arg(path)); + Pass.show(path); + } + } + } + + } + + } + + ListView { + id: passwordListSearch + + anchors.top: passwordListHeader.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + visible: passwordListNav.model.count !== 0 && !passwordListHeader.searchBarIsActive + + model: ListModel { + } + + delegate: Component { + FileDir { + fName: fileName + fIsDir: false + onClicked: { + var path = __passwordStorePath + "/" + fileName; + console.debug("pass show %1".arg(path)); + Pass.show(path); + } + } + } } @@ -105,6 +178,8 @@ Page { header: MainHeader { id: passwordListHeader + onSearchBarActived: __searchUpdateModel("") + onSearchBarTextChanged: __searchUpdateModel(text) leadingActionBar.height: units.gu(4) leadingActionBar.actions: [ Action { @@ -114,9 +189,9 @@ Page { text: i18n.tr("Back") visible: false onTriggered: { - folderModel.folder = folderModel.parentFolder; - console.debug(folderModel.folder); - if (folderModel.rootFolder === folderModel.folder) { + passwordListNav.model.folder = passwordListNav.model.parentFolder; + console.debug(passwordListNav.model.folder); + if (passwordListNav.model.rootFolder === passwordListNav.model.folder) { backAction.visible = false; passwordListHeader.title = i18n.tr("UTPass"); } else { diff --git a/qml/pages/headers/MainHeader.qml b/qml/pages/headers/MainHeader.qml index 4d50fac..4ca89d2 100644 --- a/qml/pages/headers/MainHeader.qml +++ b/qml/pages/headers/MainHeader.qml @@ -4,23 +4,29 @@ import QtQuick 2.4 PageHeader { id: mainHeader + readonly property bool searchBarIsActive: !searchBar.visible + + signal searchBarActived() + signal searchBarTextChanged(string text) + width: parent.width height: units.gu(6) title: i18n.tr("UTPass") trailingActionBar.height: units.gu(4) trailingActionBar.numberOfSlots: 2 trailingActionBar.actions: [ - /*Action { TODO - iconName: "search" + Action { + iconName: searchBarIsActive ? "search" : "close" text: i18n.tr("Search") onTriggered: { - searchBar.visible = !searchBar.visible - labelTitle.visible = !searchBar.visible + searchBar.visible = !searchBar.visible; + labelTitle.visible = !searchBar.visible; if (searchBar.visible === true) { - searchBar.focus = true + searchBar.focus = true; + searchBarActived(); } } - },*/ + }, Action { iconName: "settings" text: i18n.tr("Settings") @@ -58,8 +64,7 @@ PageHeader { height: units.gu(4) visible: false anchors.verticalCenter: parent.verticalCenter - onFocusChanged: { - } + onContentWidthChanged: searchBarTextChanged(searchBar.text) } } diff --git a/tests.in.cpp b/tests.in.cpp index 45f1dbe..57b8ba0 100644 --- a/tests.in.cpp +++ b/tests.in.cpp @@ -16,5 +16,5 @@ int main(int argc, char *argv[]) QGuiApplication::setApplicationName("utpass.qrouland"); - return quick_test_main(argc, argv, @TESTS_PATH@, @TESTS_PATH@); + return quick_test_main(argc, argv, "@TESTS_PATH@", "@TESTS_PATH@"); } diff --git a/tests/plugins/TestsUtils/CMakeLists.txt b/tests/plugins/TestsUtils/CMakeLists.txt index d53056b..2db5bce 100644 --- a/tests/plugins/TestsUtils/CMakeLists.txt +++ b/tests/plugins/TestsUtils/CMakeLists.txt @@ -5,7 +5,6 @@ set( SRC plugin.cpp utils.cpp - passphraseprovider.h ) set(CMAKE_AUTOMOC ON) @@ -23,30 +22,6 @@ endif() add_library(${PLUGIN} MODULE ${SRC}) set_target_properties(${PLUGIN} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PLUGIN}) qt5_use_modules(${PLUGIN} Qml Quick DBus) -set(RNP_BUILD_DIR "${CMAKE_SOURCE_DIR}/build/${ARCH_TRIPLET}/rnp/install") - -INCLUDE_DIRECTORIES(${RNP_BUILD_DIR}/include) - -add_library(rnp STATIC IMPORTED) -set_property(TARGET rnp PROPERTY IMPORTED_LOCATION "${RNP_BUILD_DIR}/lib/librnp.a") - -add_library(gpgerror SHARED IMPORTED) -set_property(TARGET gpgerror PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpg-error.so.0.28.0") - -add_library(libassuan SHARED IMPORTED) -set_property(TARGET libassuan PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libassuan.so") - -add_library(libgpgme SHARED IMPORTED) -set_property(TARGET libgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgme.so") - -add_library(libgpgmepp SHARED IMPORTED) -set_property(TARGET libgpgmepp PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libgpgmepp.so") - -add_library(libqgpgme SHARED IMPORTED) -set_property(TARGET libqgpgme PROPERTY IMPORTED_LOCATION "/usr/lib/${ARCH_TRIPLET}/libqgpgme.so") - - -target_link_libraries(${PLUGIN} rnp gpgerror libassuan libgpgme libgpgmepp libqgpgme) set(QT_IMPORTS_DIR "/lib/${ARCH_TRIPLET}") diff --git a/tests/plugins/TestsUtils/passphraseprovider.h b/tests/plugins/TestsUtils/passphraseprovider.h index 8a5bb8b..fd1f4ce 100644 --- a/tests/plugins/TestsUtils/passphraseprovider.h +++ b/tests/plugins/TestsUtils/passphraseprovider.h @@ -3,9 +3,9 @@ #include #include -extern "C" { -#include -} +// extern "C" { +// #include +// } class TesTPassphraseProvider : public QObject { diff --git a/tests/plugins/TestsUtils/utils.cpp b/tests/plugins/TestsUtils/utils.cpp index 86e6f49..ddf73b9 100644 --- a/tests/plugins/TestsUtils/utils.cpp +++ b/tests/plugins/TestsUtils/utils.cpp @@ -6,11 +6,11 @@ #include #include -#include "passphraseprovider.h" +//#include "passphraseprovider.h" #include "utils.h" -TestsUtils::TestsUtils(): - m_passphrase_povider(std::unique_ptr(new TesTPassphraseProvider())) +TestsUtils::TestsUtils() +//m_passphrase_povider(std::unique_ptr(new TesTPassphraseProvider())) {} @@ -68,9 +68,9 @@ void TestsUtils::copyFolder(QUrl sourceFolderUrl, QUrl destFolderUrl) } } -QObject *TestsUtils::getTestPassphraseProvider() -{ - return &TesTPassphraseProvider::instance(); -} +// QObject *TestsUtils::getTestPassphraseProvider() +// { +// return &TesTPassphraseProvider::instance(); +// } diff --git a/tests/plugins/TestsUtils/utils.h b/tests/plugins/TestsUtils/utils.h index 28f39a8..38eda88 100644 --- a/tests/plugins/TestsUtils/utils.h +++ b/tests/plugins/TestsUtils/utils.h @@ -1,7 +1,7 @@ #ifndef TESTSUTILS_H #define TESTSUTILS_H -#include "passphraseprovider.h" +//#include "passphraseprovider.h" #include #include #include @@ -18,7 +18,7 @@ public: Q_INVOKABLE QString getTempPath(); Q_INVOKABLE bool fileExists(QUrl path); Q_INVOKABLE void copyFolder(QUrl sourceFolder, QUrl destFolder); - Q_INVOKABLE QObject *getTestPassphraseProvider(); + //Q_INVOKABLE QObject *getTestPassphraseProvider(); }; diff --git a/tests/units/pass/PassTestCase.qml b/tests/units/pass/PassTestCase.qml index 6db3197..fbc3af6 100644 --- a/tests/units/pass/PassTestCase.qml +++ b/tests/units/pass/PassTestCase.qml @@ -4,6 +4,8 @@ import QtTest 1.2 import TestsUtils 1.0 TestCase { + //Pass.passphrase_provider = TestsUtils.getTestPassphraseProvider(); + property string password_store property string gpg_home @@ -13,7 +15,6 @@ TestCase { Pass.gpg_home = gpg_home; password_store = TestsUtils.getTempPath(); Pass.password_store = password_store; - Pass.passphrase_provider = TestsUtils.getTestPassphraseProvider(); } } diff --git a/tests/units/pass/tst_ls.qml b/tests/units/pass/tst_ls.qml new file mode 100644 index 0000000..c46df24 --- /dev/null +++ b/tests/units/pass/tst_ls.qml @@ -0,0 +1,44 @@ +import Pass 1.0 +import QtQuick 2.9 +import QtTest 1.2 +import TestsUtils 1.0 + +PassTestCase { + //TODO some additionanl error test + + function init_data() { + return [{ + "spy": lsSucceed, + "add_home_gpg_data": true, + "passwords": ["test.gpg"] + }, { + "spy": lsSucceed, + "add_home_gpg_data": false, + "passwords": [] + }]; + } + + function test_ls(data) { + if (data.add_home_gpg_data === true) + TestsUtils.copyFolder(Qt.resolvedUrl("../../assets/password-store"), Qt.resolvedUrl(password_store)); + + var passwords; + Pass.lsSucceed.connect(function(ret) { + passwords = ret; + }); + Pass.ls(); + data.spy.wait(); + verify(passwords.length === data.passwords.length, "Should return %1 password(s) but return %2 password(s)".arg(data.nb_password).arg(passwords.length)); + for (var i = 0; data.passwords.length; i++) { + verify(passwords[i] === data.passwords[i], "%1 name should be %2 but is %3".arg(i).arg(data.passwords[i]).arg(passwords[i])); + } + } + + SignalSpy { + id: lsSucceed + + target: Pass + signalName: "lsSucceed" + } + +} diff --git a/tests/units/pass/tst_show.qml b/tests/units/pass/tst_show.qml index 6e05733..ed1b3fe 100644 --- a/tests/units/pass/tst_show.qml +++ b/tests/units/pass/tst_show.qml @@ -4,24 +4,24 @@ import QtTest 1.2 import TestsUtils 1.0 PassTestCase { + // TODO This test need to fixed by providing custom stub password provider to the pass plugin for succeed tests //TODO some additionanl error test - function init_data() { return [{ "spy": showFailed, "err_msg": "Bad password", - "add_home_gpg_data": true, + "add_password_store_data": true, "file": "../../assets/gpg/clear_text.txt.gpg" }, { "spy": showFailed, "err_msg": "No suitable key", - "add_home_gpg_data": false, + "add_password_store_data": false, "file": "../../assets/gpg/clear_text.txt.gpg" }]; } function test_pass_show(data) { - if (data.add_home_gpg_data === true) + if (data.add_password_store_data === true) TestsUtils.copyFolder(Qt.resolvedUrl("../../assets/gpghome"), Qt.resolvedUrl(gpg_home)); var fname, ctext;