mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Add --username option to Clip command. (#3947)
* make Clip accept an attribute name This allows users to copy arbitrary attributes (e.g. username, notes, URL) to the clipboard in addition to the password and TOTP values. * update Clip manpage * Add findAttributes to CLI utils * Use case-insensitive search in Show command. * Use case-insensitive search in Clip command. Co-authored-by: louib <L0U13@protonmail.com>
This commit is contained in:
parent
06e0f38523
commit
71a39c37ec
@ -23,7 +23,7 @@ The same password generation options as documented for the generate command can
|
||||
Analyzes passwords in a database for weaknesses.
|
||||
|
||||
.IP "\fBclip\fP [options] <database> <entry> [timeout]"
|
||||
Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password 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.
|
||||
Copies an attribute or the current TOTP (if the \fI-t\fP option is specified) of a database entry to the clipboard. If no attribute name is specified using the \fI-a\fP 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.
|
||||
|
||||
.IP "\fBclose\fP"
|
||||
In interactive mode, closes the currently opened database (see \fIopen\fP).
|
||||
@ -174,10 +174,14 @@ hour or so).
|
||||
|
||||
.SS "Clip options"
|
||||
|
||||
.IP "\fB-t\fP, \fB--totp\fP"
|
||||
Copies the current TOTP instead of current password to clipboard. Will report
|
||||
an error if no TOTP is configured for the entry.
|
||||
.IP "\fB-a\fP, \fB--attribute\fP"
|
||||
Copies the specified attribute to the clipboard. If no attribute is specified,
|
||||
the password attribute is the default. For example, "\fI-a\fP username" would
|
||||
copy the username to the clipboard. [Default: password]
|
||||
|
||||
.IP "\fB-t\fP, \fB--totp\fP"
|
||||
Copies the current TOTP instead of the specified attribute to the clipboard.
|
||||
Will report an error if no TOTP is configured for the entry.
|
||||
|
||||
.SS "Create options"
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <stdio.h>
|
||||
#include <thread>
|
||||
|
||||
#include "Clip.h"
|
||||
@ -28,14 +27,23 @@
|
||||
#include "core/Entry.h"
|
||||
#include "core/Group.h"
|
||||
|
||||
const QCommandLineOption Clip::TotpOption = QCommandLineOption(QStringList() << "t"
|
||||
<< "totp",
|
||||
QObject::tr("Copy the current TOTP to the clipboard."));
|
||||
const QCommandLineOption Clip::AttributeOption = QCommandLineOption(
|
||||
QStringList() << "a"
|
||||
<< "attribute",
|
||||
QObject::tr("Copy the given attribute to the clipboard. Defaults to \"password\" if not specified."),
|
||||
"attr",
|
||||
"password");
|
||||
|
||||
const QCommandLineOption Clip::TotpOption =
|
||||
QCommandLineOption(QStringList() << "t"
|
||||
<< "totp",
|
||||
QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\")."));
|
||||
|
||||
Clip::Clip()
|
||||
{
|
||||
name = QString("clip");
|
||||
description = QObject::tr("Copy an entry's password to the clipboard.");
|
||||
description = QObject::tr("Copy an entry's attribute to the clipboard.");
|
||||
options.append(Clip::AttributeOption);
|
||||
options.append(Clip::TotpOption);
|
||||
positionalArguments.append(
|
||||
{QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")});
|
||||
@ -51,7 +59,6 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
||||
if (args.size() == 3) {
|
||||
timeout = args.at(2);
|
||||
}
|
||||
bool clipTotp = parser->isSet(Clip::TotpOption);
|
||||
TextStream errorTextStream(Utils::STDERR);
|
||||
|
||||
int timeoutSeconds = 0;
|
||||
@ -70,16 +77,39 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (parser->isSet(AttributeOption) && parser->isSet(TotpOption)) {
|
||||
errorTextStream << QObject::tr("ERROR: Please specify one of --attribute or --totp, not both.") << endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
QString selectedAttribute = parser->value(AttributeOption);
|
||||
QString value;
|
||||
if (clipTotp) {
|
||||
bool found = false;
|
||||
if (parser->isSet(TotpOption) || selectedAttribute == "totp") {
|
||||
if (!entry->hasTotp()) {
|
||||
errorTextStream << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
found = true;
|
||||
value = entry->totp();
|
||||
} else {
|
||||
value = entry->password();
|
||||
QStringList attrs = Utils::findAttributes(*entry->attributes(), selectedAttribute);
|
||||
if (attrs.size() > 1) {
|
||||
errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.")
|
||||
.arg(selectedAttribute, QLocale().createSeparatedList(attrs))
|
||||
<< endl;
|
||||
return EXIT_FAILURE;
|
||||
} else if (attrs.size() == 1) {
|
||||
found = true;
|
||||
selectedAttribute = attrs[0];
|
||||
value = entry->attributes()->value(selectedAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
outputTextStream << QObject::tr("Attribute \"%1\" not found.").arg(selectedAttribute) << endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
int exitCode = Utils::clipText(value);
|
||||
@ -87,11 +117,7 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
if (clipTotp) {
|
||||
outputTextStream << QObject::tr("Entry's current TOTP copied to the clipboard!") << endl;
|
||||
} else {
|
||||
outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl;
|
||||
}
|
||||
outputTextStream << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl;
|
||||
|
||||
if (!timeoutSeconds) {
|
||||
return exitCode;
|
||||
|
@ -27,6 +27,7 @@ public:
|
||||
|
||||
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
|
||||
|
||||
static const QCommandLineOption AttributeOption;
|
||||
static const QCommandLineOption TotpOption;
|
||||
};
|
||||
|
||||
|
@ -27,6 +27,8 @@
|
||||
#include "core/Global.h"
|
||||
#include "core/Group.h"
|
||||
|
||||
#include <QLocale>
|
||||
|
||||
const QCommandLineOption Show::TotpOption = QCommandLineOption(QStringList() << "t"
|
||||
<< "totp",
|
||||
QObject::tr("Show the entry's current TOTP."));
|
||||
@ -79,25 +81,33 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
||||
|
||||
// If no attributes specified, output the default attribute set.
|
||||
bool showDefaultAttributes = attributes.isEmpty() && !showTotp;
|
||||
if (attributes.isEmpty() && !showTotp) {
|
||||
if (showDefaultAttributes) {
|
||||
attributes = EntryAttributes::DefaultAttributes;
|
||||
}
|
||||
|
||||
// Iterate over the attributes and output them line-by-line.
|
||||
bool sawUnknownAttribute = false;
|
||||
bool encounteredError = false;
|
||||
for (const QString& attributeName : asConst(attributes)) {
|
||||
if (!entry->attributes()->contains(attributeName)) {
|
||||
sawUnknownAttribute = true;
|
||||
QStringList attrs = Utils::findAttributes(*entry->attributes(), attributeName);
|
||||
if (attrs.isEmpty()) {
|
||||
encounteredError = true;
|
||||
errorTextStream << QObject::tr("ERROR: unknown attribute %1.").arg(attributeName) << endl;
|
||||
continue;
|
||||
} else if (attrs.size() > 1) {
|
||||
encounteredError = true;
|
||||
errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.")
|
||||
.arg(attributeName, QLocale().createSeparatedList(attrs))
|
||||
<< endl;
|
||||
continue;
|
||||
}
|
||||
QString canonicalName = attrs[0];
|
||||
if (showDefaultAttributes) {
|
||||
outputTextStream << attributeName << ": ";
|
||||
outputTextStream << canonicalName << ": ";
|
||||
}
|
||||
if (entry->attributes()->isProtected(attributeName) && showDefaultAttributes && !showProtectedAttributes) {
|
||||
if (entry->attributes()->isProtected(canonicalName) && showDefaultAttributes && !showProtectedAttributes) {
|
||||
outputTextStream << "PROTECTED" << endl;
|
||||
} else {
|
||||
outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attributeName)) << endl;
|
||||
outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(canonicalName)) << endl;
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,5 +115,5 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
|
||||
outputTextStream << entry->totp() << endl;
|
||||
}
|
||||
|
||||
return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
|
@ -331,4 +331,21 @@ namespace Utils
|
||||
return result;
|
||||
}
|
||||
|
||||
QStringList findAttributes(const EntryAttributes& attributes, const QString& name)
|
||||
{
|
||||
QStringList result;
|
||||
if (attributes.hasKey(name)) {
|
||||
result.append(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const QString& key : attributes.keys()) {
|
||||
if (key.compare(name, Qt::CaseSensitivity::CaseInsensitive) == 0) {
|
||||
result.append(key);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Utils
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
#include "cli/TextStream.h"
|
||||
#include "core/Database.h"
|
||||
#include "core/EntryAttributes.h"
|
||||
#include "keys/CompositeKey.h"
|
||||
#include "keys/FileKey.h"
|
||||
#include "keys/PasswordKey.h"
|
||||
@ -51,6 +52,14 @@ namespace Utils
|
||||
|
||||
QStringList splitCommandString(const QString& command);
|
||||
|
||||
/**
|
||||
* If `attributes` contains an attribute named `name` (case-sensitive),
|
||||
* returns a list containing only `name`. Otherwise, returns the list of
|
||||
* all attribute names in `attributes` matching the given name
|
||||
* (case-insensitive).
|
||||
*/
|
||||
QStringList findAttributes(const EntryAttributes& attributes, const QString& name);
|
||||
|
||||
namespace Test
|
||||
{
|
||||
void setNextPassword(const QString& password);
|
||||
|
@ -480,7 +480,7 @@ void TestCli::testClip()
|
||||
|
||||
QCOMPARE(clipboard->text(), QString("Password"));
|
||||
m_stdoutFile->readLine(); // skip prompt line
|
||||
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's password copied to the clipboard!\n"));
|
||||
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n"));
|
||||
|
||||
// Quiet option
|
||||
qint64 pos = m_stdoutFile->pos();
|
||||
@ -491,6 +491,11 @@ void TestCli::testClip()
|
||||
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||
QCOMPARE(clipboard->text(), QString("Password"));
|
||||
|
||||
// Username
|
||||
Utils::Test::setNextPassword("a");
|
||||
clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "-a", "username"});
|
||||
QCOMPARE(clipboard->text(), QString("User Name"));
|
||||
|
||||
// TOTP
|
||||
Utils::Test::setNextPassword("a");
|
||||
clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"});
|
||||
@ -538,6 +543,20 @@ void TestCli::testClip()
|
||||
clipCmd.execute({"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"});
|
||||
m_stderrFile->seek(posErr);
|
||||
QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n"));
|
||||
|
||||
posErr = m_stderrFile->pos();
|
||||
Utils::Test::setNextPassword("a");
|
||||
clipCmd.execute({"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry"});
|
||||
m_stderrFile->seek(posErr);
|
||||
QCOMPARE(
|
||||
m_stderrFile->readAll(),
|
||||
QByteArray("ERROR: attribute TESTAttribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
|
||||
|
||||
posErr = m_stderrFile->pos();
|
||||
Utils::Test::setNextPassword("a");
|
||||
clipCmd.execute({"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"});
|
||||
m_stderrFile->seek(posErr);
|
||||
QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: Please specify one of --attribute or --totp, not both.\n"));
|
||||
}
|
||||
|
||||
void TestCli::testCreate()
|
||||
@ -1913,6 +1932,16 @@ void TestCli::testShow()
|
||||
QByteArray("Sample Entry\n"
|
||||
"http://www.somesite.com/\n"));
|
||||
|
||||
// Test case insensitivity
|
||||
pos = m_stdoutFile->pos();
|
||||
Utils::Test::setNextPassword("a");
|
||||
showCmd.execute({"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"});
|
||||
m_stdoutFile->seek(pos);
|
||||
m_stdoutFile->readLine(); // skip password prompt
|
||||
QCOMPARE(m_stdoutFile->readAll(),
|
||||
QByteArray("Sample Entry\n"
|
||||
"http://www.somesite.com/\n"));
|
||||
|
||||
pos = m_stdoutFile->pos();
|
||||
Utils::Test::setNextPassword("a");
|
||||
showCmd.execute({"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"});
|
||||
@ -1946,6 +1975,19 @@ void TestCli::testShow()
|
||||
m_stderrFile->seek(posErr);
|
||||
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||
QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n"));
|
||||
|
||||
// Show with ambiguous attributes
|
||||
pos = m_stdoutFile->pos();
|
||||
posErr = m_stderrFile->pos();
|
||||
Utils::Test::setNextPassword("a");
|
||||
showCmd.execute({"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"});
|
||||
m_stdoutFile->seek(pos);
|
||||
m_stdoutFile->readLine(); // skip password prompt
|
||||
m_stderrFile->seek(posErr);
|
||||
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
|
||||
QCOMPARE(
|
||||
m_stderrFile->readAll(),
|
||||
QByteArray("ERROR: attribute Testattribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n"));
|
||||
}
|
||||
|
||||
void TestCli::testInvalidDbFiles()
|
||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user