From 8a7be101e443a9d85b77da1ebdd74db1811b8894 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 18 Apr 2021 16:11:57 -0400 Subject: [PATCH] CLI Improvements * Fix #6001 - only use `--notes` in Add/Edit commands to prevent clash with password generator option `-n`. * Fix #6119 - Send Unicode to clip command; Windows only understands UTF-16 encoding. * Fix #6128 - `clip` command will default to clearing the clipboard after 10 seconds. To disable clearing set timeout to 0. --- docs/man/keepassxc-cli.1.adoc | 4 ++-- src/cli/Add.cpp | 6 ++--- src/cli/Clip.cpp | 44 +++++++++++++++++----------------- src/cli/Utils.cpp | 9 ++++++- tests/TestCli.cpp | 36 ++++++++++++++-------------- tests/data/NewDatabase2.kdbx | Bin 8350 -> 10750 bytes 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc index d3aba6120..906e45e05 100644 --- a/docs/man/keepassxc-cli.1.adoc +++ b/docs/man/keepassxc-cli.1.adoc @@ -45,7 +45,7 @@ It provides the ability to query and modify the entries of a KeePass database, d If no attribute name is specified using the *-a* option, the password is copied. If multiple entries with the same name exist in different groups, only the attribute for the first one is copied. For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. - Optionally, a timeout in seconds can be specified to automatically clear the clipboard. + Optionally, a timeout in seconds can be specified to automatically clear the clipboard, the default timeout is 10 seconds, set to 0 to disable. *close*:: In interactive mode, closes the currently opened database (see *open*). @@ -182,7 +182,7 @@ The same password generation options as documented for the generate command can *--url* <__url__>:: Specifies the URL of the entry. -*-n*, *--notes* <__notes__>:: +*--notes* <__notes__>:: Specifies the notes of the entry. *-p*, *--password-prompt*:: diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index 2f4f14d69..6ff4fa36a 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -30,10 +30,8 @@ const QCommandLineOption Add::UsernameOption = QCommandLineOption(QStringList() const QCommandLineOption Add::UrlOption = QCommandLineOption(QStringList() << "url", QObject::tr("URL for the entry."), QObject::tr("URL")); -const QCommandLineOption Add::NotesOption = QCommandLineOption(QStringList() << "n" - << "notes", - QObject::tr("Notes for the entry."), - QObject::tr("Notes")); +const QCommandLineOption Add::NotesOption = + QCommandLineOption(QStringList() << "notes", QObject::tr("Notes for the entry."), QObject::tr("Notes")); const QCommandLineOption Add::PasswordPromptOption = QCommandLineOption(QStringList() << "p" diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 1096d4ff6..10865092f 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -22,6 +22,8 @@ #include "core/Group.h" #include "core/Tools.h" +#define CLI_DEFAULT_CLIP_TIMEOUT 10 + const QCommandLineOption Clip::AttributeOption = QCommandLineOption( QStringList() << "a" << "attribute", @@ -50,7 +52,10 @@ Clip::Clip() positionalArguments.append( {QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")}); optionalArguments.append( - {QString("timeout"), QObject::tr("Timeout in seconds before clearing the clipboard."), QString("[timeout]")}); + {QString("timeout"), + QObject::tr("Timeout before clearing the clipboard (default is %1 seconds, set to 0 for unlimited).") + .arg(CLI_DEFAULT_CLIP_TIMEOUT), + QString("[timeout]")}); } int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer parser) @@ -59,13 +64,18 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer< auto& err = Utils::STDERR; const QStringList args = parser->positionalArguments(); - QString bestEntryPath; - QString timeout; + auto timeout = CLI_DEFAULT_CLIP_TIMEOUT; if (args.size() == 3) { - timeout = args.at(2); + bool ok; + timeout = args.at(2).toInt(&ok); + if (!ok) { + err << QObject::tr("Invalid timeout value %1.").arg(args.at(2)) << endl; + return EXIT_FAILURE; + } } + QString entryPath; if (parser->isSet(Clip::BestMatchOption)) { QStringList results = database->rootGroup()->locate(args.at(1)); if (results.count() > 1) { @@ -75,24 +85,14 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer< } return EXIT_FAILURE; } else { - bestEntryPath = (results.isEmpty()) ? args.at(1) : results[0]; - out << QObject::tr("Used matching entry: %1").arg(bestEntryPath) << endl; + entryPath = (results.isEmpty()) ? args.at(1) : results[0]; + out << QObject::tr("Used matching entry: %1").arg(entryPath) << endl; } } else { - bestEntryPath = args.at(1); + entryPath = args.at(1); } - const QString& entryPath = bestEntryPath; - - int timeoutSeconds = 0; - if (!timeout.isEmpty() && timeout.toInt() <= 0) { - err << QObject::tr("Invalid timeout value %1.").arg(timeout) << endl; - return EXIT_FAILURE; - } else if (!timeout.isEmpty()) { - timeoutSeconds = timeout.toInt(); - } - - Entry* entry = database->rootGroup()->findEntryByPath(entryPath); + auto* entry = database->rootGroup()->findEntryByPath(entryPath); if (!entry) { err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl; return EXIT_FAILURE; @@ -140,17 +140,17 @@ int Clip::executeWithDatabase(QSharedPointer database, QSharedPointer< out << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl; - if (!timeoutSeconds) { + if (timeout <= 0) { return exitCode; } QString lastLine = ""; - while (timeoutSeconds > 0) { + while (timeout > 0) { out << '\r' << QString(lastLine.size(), ' ') << '\r'; - lastLine = QObject::tr("Clearing the clipboard in %1 second(s)…", "", timeoutSeconds).arg(timeoutSeconds); + lastLine = QObject::tr("Clearing the clipboard in %1 second(s)...", "", timeout).arg(timeout); out << lastLine << flush; Tools::sleep(1000); - --timeoutSeconds; + --timeout; } Utils::clipText(""); out << '\r' << QString(lastLine.size(), ' ') << '\r'; diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index b1c6ca645..44ed10c56 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -308,7 +308,14 @@ namespace Utils continue; } - if (clipProcess->write(text.toLatin1()) == -1) { +#ifdef Q_OS_WIN + // Windows clip command only understands Unicode written as UTF-16 + auto data = QByteArray::fromRawData(reinterpret_cast(text.utf16()), text.size() * 2); + if (clipProcess->write(data) == -1) { +#else + // Other platforms understand UTF-8 + if (clipProcess->write(text.toUtf8()) == -1) { +#endif qDebug("Unable to write to process : %s", qPrintable(clipProcess->errorString())); } clipProcess->waitForBytesWritten(); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index b01e9576e..8a14cb88b 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -284,7 +284,7 @@ void TestCli::testAdd() "-g", "-L", "20", - "-n", + "--notes", "some notes", m_dbFile->fileName(), "/newuser-entry"}); @@ -360,7 +360,7 @@ void TestCli::testAdd() QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch()); setInput("a"); - execCmd(addCmd, {"add", "-u", "newuser5", "-n", "test\\nnew line", m_dbFile->fileName(), "/newuser-entry5"}); + execCmd(addCmd, {"add", "-u", "newuser5", "--notes", "test\\nnew line", m_dbFile->fileName(), "/newuser-entry5"}); m_stderr->readLine(); // skip password prompt QCOMPARE(m_stderr->readAll(), QByteArray("")); QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n")); @@ -446,7 +446,7 @@ void TestCli::testClip() // Password setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry"}); + execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0"}); QString errorOutput(m_stderr->readAll()); if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) { @@ -465,22 +465,26 @@ void TestCli::testClip() // Quiet option setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "-q"}); + execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-q"}); QCOMPARE(m_stderr->readAll(), QByteArray()); QCOMPARE(m_stdout->readAll(), QByteArray()); QTRY_COMPARE(clipboard->text(), QString("Password")); // Username setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "-a", "username"}); + execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "-a", "username"}); QTRY_COMPARE(clipboard->text(), QString("User Name")); // TOTP setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"}); - + execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0", "--totp"}); QTRY_VERIFY(isTotp(clipboard->text())); + // Test Unicode + setInput("a"); + execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "/General/Unicode", "0", "-a", "username"}); + QTRY_COMPARE(clipboard->text(), QString(R"(¯\_(ツ)_/¯)")); + // Password with timeout setInput("a"); // clang-format off @@ -505,35 +509,31 @@ void TestCli::testClip() future.waitForFinished(); - setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "0"}); - QVERIFY(m_stderr->readAll().contains("Invalid timeout value 0.\n")); - setInput("a"); execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"}); QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n")); setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"}); + execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry", "0"}); QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n")); setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry"}); + execCmd(clipCmd, {"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry", "0"}); QVERIFY(m_stderr->readAll().contains("ERROR: attribute TESTAttribute1 is ambiguous")); setInput("a"); - execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"}); + execCmd(clipCmd, {"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry", "0"}); QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n")); // Best option setInput("a"); - execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "-b"}); + execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "0", "-b"}); QByteArray errorChoices = m_stderr->readAll(); QVERIFY(errorChoices.contains("Multi Entry 1")); QVERIFY(errorChoices.contains("Multi Entry 2")); setInput("a"); - execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "-b"}); + execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Entry 2", "0", "-b"}); QTRY_COMPARE(clipboard->text(), QString("Password2")); } @@ -750,7 +750,7 @@ void TestCli::testEdit() "newuser", "--url", "https://otherurl.example.com/", - "-n", + "--notes", "newnotes", "-t", "newtitle", @@ -825,7 +825,7 @@ void TestCli::testEdit() // with line break in notes setInput("a"); - execCmd(editCmd, {"edit", m_dbFile->fileName(), "-n", "testing\\nline breaks", "/evennewertitle"}); + execCmd(editCmd, {"edit", m_dbFile->fileName(), "--notes", "testing\\nline breaks", "/evennewertitle"}); db = readDatabase(); entry = db->rootGroup()->findEntryByPath("/evennewertitle"); QVERIFY(entry); diff --git a/tests/data/NewDatabase2.kdbx b/tests/data/NewDatabase2.kdbx index 4e77724c7e8cfdf22bcd951ccfcaa4ae7f49d460..2ff491cc70ea87b77d0ae3f5d062819e3edff8f0 100644 GIT binary patch literal 10750 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 literal 8350 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