mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 23:09:44 -05:00
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.
This commit is contained in:
parent
be3e77d721
commit
8a7be101e4
@ -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 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.
|
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.
|
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*::
|
*close*::
|
||||||
In interactive mode, closes the currently opened database (see *open*).
|
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__>::
|
*--url* <__url__>::
|
||||||
Specifies the URL of the entry.
|
Specifies the URL of the entry.
|
||||||
|
|
||||||
*-n*, *--notes* <__notes__>::
|
*--notes* <__notes__>::
|
||||||
Specifies the notes of the entry.
|
Specifies the notes of the entry.
|
||||||
|
|
||||||
*-p*, *--password-prompt*::
|
*-p*, *--password-prompt*::
|
||||||
|
@ -30,10 +30,8 @@ const QCommandLineOption Add::UsernameOption = QCommandLineOption(QStringList()
|
|||||||
const QCommandLineOption Add::UrlOption =
|
const QCommandLineOption Add::UrlOption =
|
||||||
QCommandLineOption(QStringList() << "url", QObject::tr("URL for the entry."), QObject::tr("URL"));
|
QCommandLineOption(QStringList() << "url", QObject::tr("URL for the entry."), QObject::tr("URL"));
|
||||||
|
|
||||||
const QCommandLineOption Add::NotesOption = QCommandLineOption(QStringList() << "n"
|
const QCommandLineOption Add::NotesOption =
|
||||||
<< "notes",
|
QCommandLineOption(QStringList() << "notes", QObject::tr("Notes for the entry."), QObject::tr("Notes"));
|
||||||
QObject::tr("Notes for the entry."),
|
|
||||||
QObject::tr("Notes"));
|
|
||||||
|
|
||||||
const QCommandLineOption Add::PasswordPromptOption =
|
const QCommandLineOption Add::PasswordPromptOption =
|
||||||
QCommandLineOption(QStringList() << "p"
|
QCommandLineOption(QStringList() << "p"
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
|
|
||||||
|
#define CLI_DEFAULT_CLIP_TIMEOUT 10
|
||||||
|
|
||||||
const QCommandLineOption Clip::AttributeOption = QCommandLineOption(
|
const QCommandLineOption Clip::AttributeOption = QCommandLineOption(
|
||||||
QStringList() << "a"
|
QStringList() << "a"
|
||||||
<< "attribute",
|
<< "attribute",
|
||||||
@ -50,7 +52,10 @@ Clip::Clip()
|
|||||||
positionalArguments.append(
|
positionalArguments.append(
|
||||||
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
|
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
|
||||||
optionalArguments.append(
|
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> database, QSharedPointer<QCommandLineParser> parser)
|
int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
|
||||||
@ -59,13 +64,18 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
|||||||
auto& err = Utils::STDERR;
|
auto& err = Utils::STDERR;
|
||||||
|
|
||||||
const QStringList args = parser->positionalArguments();
|
const QStringList args = parser->positionalArguments();
|
||||||
QString bestEntryPath;
|
|
||||||
|
|
||||||
QString timeout;
|
auto timeout = CLI_DEFAULT_CLIP_TIMEOUT;
|
||||||
if (args.size() == 3) {
|
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)) {
|
if (parser->isSet(Clip::BestMatchOption)) {
|
||||||
QStringList results = database->rootGroup()->locate(args.at(1));
|
QStringList results = database->rootGroup()->locate(args.at(1));
|
||||||
if (results.count() > 1) {
|
if (results.count() > 1) {
|
||||||
@ -75,24 +85,14 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
|||||||
}
|
}
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
} else {
|
} else {
|
||||||
bestEntryPath = (results.isEmpty()) ? args.at(1) : results[0];
|
entryPath = (results.isEmpty()) ? args.at(1) : results[0];
|
||||||
out << QObject::tr("Used matching entry: %1").arg(bestEntryPath) << endl;
|
out << QObject::tr("Used matching entry: %1").arg(entryPath) << endl;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bestEntryPath = args.at(1);
|
entryPath = args.at(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString& entryPath = bestEntryPath;
|
auto* entry = database->rootGroup()->findEntryByPath(entryPath);
|
||||||
|
|
||||||
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);
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl;
|
err << QObject::tr("Entry %1 not found.").arg(entryPath) << endl;
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
@ -140,17 +140,17 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
|||||||
|
|
||||||
out << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl;
|
out << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl;
|
||||||
|
|
||||||
if (!timeoutSeconds) {
|
if (timeout <= 0) {
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString lastLine = "";
|
QString lastLine = "";
|
||||||
while (timeoutSeconds > 0) {
|
while (timeout > 0) {
|
||||||
out << '\r' << QString(lastLine.size(), ' ') << '\r';
|
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;
|
out << lastLine << flush;
|
||||||
Tools::sleep(1000);
|
Tools::sleep(1000);
|
||||||
--timeoutSeconds;
|
--timeout;
|
||||||
}
|
}
|
||||||
Utils::clipText("");
|
Utils::clipText("");
|
||||||
out << '\r' << QString(lastLine.size(), ' ') << '\r';
|
out << '\r' << QString(lastLine.size(), ' ') << '\r';
|
||||||
|
@ -308,7 +308,14 @@ namespace Utils
|
|||||||
continue;
|
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<const char*>(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()));
|
qDebug("Unable to write to process : %s", qPrintable(clipProcess->errorString()));
|
||||||
}
|
}
|
||||||
clipProcess->waitForBytesWritten();
|
clipProcess->waitForBytesWritten();
|
||||||
|
@ -284,7 +284,7 @@ void TestCli::testAdd()
|
|||||||
"-g",
|
"-g",
|
||||||
"-L",
|
"-L",
|
||||||
"20",
|
"20",
|
||||||
"-n",
|
"--notes",
|
||||||
"some notes",
|
"some notes",
|
||||||
m_dbFile->fileName(),
|
m_dbFile->fileName(),
|
||||||
"/newuser-entry"});
|
"/newuser-entry"});
|
||||||
@ -360,7 +360,7 @@ void TestCli::testAdd()
|
|||||||
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
|
||||||
|
|
||||||
setInput("a");
|
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
|
m_stderr->readLine(); // skip password prompt
|
||||||
QCOMPARE(m_stderr->readAll(), QByteArray(""));
|
QCOMPARE(m_stderr->readAll(), QByteArray(""));
|
||||||
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n"));
|
QCOMPARE(m_stdout->readAll(), QByteArray("Successfully added entry newuser-entry5.\n"));
|
||||||
@ -446,7 +446,7 @@ void TestCli::testClip()
|
|||||||
|
|
||||||
// Password
|
// Password
|
||||||
setInput("a");
|
setInput("a");
|
||||||
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry"});
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "/Sample Entry", "0"});
|
||||||
QString errorOutput(m_stderr->readAll());
|
QString errorOutput(m_stderr->readAll());
|
||||||
|
|
||||||
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
|
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
|
||||||
@ -465,22 +465,26 @@ void TestCli::testClip()
|
|||||||
|
|
||||||
// Quiet option
|
// Quiet option
|
||||||
setInput("a");
|
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_stderr->readAll(), QByteArray());
|
||||||
QCOMPARE(m_stdout->readAll(), QByteArray());
|
QCOMPARE(m_stdout->readAll(), QByteArray());
|
||||||
QTRY_COMPARE(clipboard->text(), QString("Password"));
|
QTRY_COMPARE(clipboard->text(), QString("Password"));
|
||||||
|
|
||||||
// Username
|
// Username
|
||||||
setInput("a");
|
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"));
|
QTRY_COMPARE(clipboard->text(), QString("User Name"));
|
||||||
|
|
||||||
// TOTP
|
// TOTP
|
||||||
setInput("a");
|
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()));
|
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
|
// Password with timeout
|
||||||
setInput("a");
|
setInput("a");
|
||||||
// clang-format off
|
// clang-format off
|
||||||
@ -505,35 +509,31 @@ void TestCli::testClip()
|
|||||||
|
|
||||||
future.waitForFinished();
|
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");
|
setInput("a");
|
||||||
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
|
execCmd(clipCmd, {"clip", m_dbFile->fileName(), "--totp", "/Sample Entry", "bleuh"});
|
||||||
QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
|
QVERIFY(m_stderr->readAll().contains("Invalid timeout value bleuh.\n"));
|
||||||
|
|
||||||
setInput("a");
|
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"));
|
QVERIFY(m_stderr->readAll().contains("Entry with path /Sample Entry has no TOTP set up.\n"));
|
||||||
|
|
||||||
setInput("a");
|
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"));
|
QVERIFY(m_stderr->readAll().contains("ERROR: attribute TESTAttribute1 is ambiguous"));
|
||||||
|
|
||||||
setInput("a");
|
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"));
|
QVERIFY(m_stderr->readAll().contains("ERROR: Please specify one of --attribute or --totp, not both.\n"));
|
||||||
|
|
||||||
// Best option
|
// Best option
|
||||||
setInput("a");
|
setInput("a");
|
||||||
execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "-b"});
|
execCmd(clipCmd, {"clip", m_dbFileMulti->fileName(), "Multi", "0", "-b"});
|
||||||
QByteArray errorChoices = m_stderr->readAll();
|
QByteArray errorChoices = m_stderr->readAll();
|
||||||
QVERIFY(errorChoices.contains("Multi Entry 1"));
|
QVERIFY(errorChoices.contains("Multi Entry 1"));
|
||||||
QVERIFY(errorChoices.contains("Multi Entry 2"));
|
QVERIFY(errorChoices.contains("Multi Entry 2"));
|
||||||
|
|
||||||
setInput("a");
|
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"));
|
QTRY_COMPARE(clipboard->text(), QString("Password2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,7 +750,7 @@ void TestCli::testEdit()
|
|||||||
"newuser",
|
"newuser",
|
||||||
"--url",
|
"--url",
|
||||||
"https://otherurl.example.com/",
|
"https://otherurl.example.com/",
|
||||||
"-n",
|
"--notes",
|
||||||
"newnotes",
|
"newnotes",
|
||||||
"-t",
|
"-t",
|
||||||
"newtitle",
|
"newtitle",
|
||||||
@ -825,7 +825,7 @@ void TestCli::testEdit()
|
|||||||
|
|
||||||
// with line break in notes
|
// with line break in notes
|
||||||
setInput("a");
|
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();
|
db = readDatabase();
|
||||||
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
|
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
|
||||||
QVERIFY(entry);
|
QVERIFY(entry);
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user