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 4e77724c7..2ff491cc7 100644 Binary files a/tests/data/NewDatabase2.kdbx and b/tests/data/NewDatabase2.kdbx differ