|
|
|
|
@ -17,7 +17,7 @@ |
|
|
|
|
*****************************************************************************/ |
|
|
|
|
#include <KSharedConfig> |
|
|
|
|
#include <KLocalizedString> |
|
|
|
|
#include <knotification.h> |
|
|
|
|
#include <KNotification> |
|
|
|
|
|
|
|
|
|
#include <QAction> |
|
|
|
|
#include <QDirIterator> |
|
|
|
|
@ -27,8 +27,9 @@ |
|
|
|
|
#include <QMessageBox> |
|
|
|
|
#include <QClipboard> |
|
|
|
|
#include <QDebug> |
|
|
|
|
#include <QApplication> |
|
|
|
|
|
|
|
|
|
#include <stdlib.h> |
|
|
|
|
#include <cstdlib> |
|
|
|
|
|
|
|
|
|
#include "pass.h" |
|
|
|
|
#include "config.h" |
|
|
|
|
@ -39,41 +40,40 @@ using namespace std; |
|
|
|
|
Pass::Pass(QObject *parent, const QVariantList &args) |
|
|
|
|
: Plasma::AbstractRunner(parent, args) |
|
|
|
|
{ |
|
|
|
|
Q_UNUSED(args); |
|
|
|
|
|
|
|
|
|
// General runner configuration
|
|
|
|
|
setObjectName(QString("Pass")); |
|
|
|
|
setObjectName(QStringLiteral("Pass")); |
|
|
|
|
setSpeed(AbstractRunner::NormalSpeed); |
|
|
|
|
setPriority(HighestPriority); |
|
|
|
|
auto comment = i18n("Looks for a password matching :q:. Pressing ENTER copies the password to the clipboard."); |
|
|
|
|
setDefaultSyntax(Plasma::RunnerSyntax(QString(":q:"), comment)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Pass::~Pass() {} |
|
|
|
|
Pass::~Pass() = default; |
|
|
|
|
|
|
|
|
|
void Pass::reloadConfiguration() |
|
|
|
|
{ |
|
|
|
|
actions().clear(); |
|
|
|
|
clearActions(); |
|
|
|
|
orderedActions.clear(); |
|
|
|
|
|
|
|
|
|
KConfigGroup cfg = config(); |
|
|
|
|
cfg.config()->reparseConfiguration(); // Just to be sure
|
|
|
|
|
this->showActions = cfg.readEntry(Config::showActions, false); |
|
|
|
|
|
|
|
|
|
if (showActions) { |
|
|
|
|
auto configActions = cfg.group(Config::Group::Actions); |
|
|
|
|
const auto configActions = cfg.group(Config::Group::Actions); |
|
|
|
|
|
|
|
|
|
// Create actions for every additional field
|
|
|
|
|
auto groups = configActions.groupList(); |
|
|
|
|
Q_FOREACH (auto name, groups) { |
|
|
|
|
const auto configActionsList = configActions.groupList(); |
|
|
|
|
for (const auto &name: configActionsList) { |
|
|
|
|
auto group = configActions.group(name); |
|
|
|
|
auto passAction = PassAction::fromConfig(group); |
|
|
|
|
|
|
|
|
|
auto icon = QIcon::fromTheme(passAction.icon, QIcon::fromTheme("object-unlocked")); |
|
|
|
|
QAction *act = addAction(passAction.name, icon , passAction.name); |
|
|
|
|
QAction *act = addAction(passAction.name, icon, passAction.name); |
|
|
|
|
act->setData(passAction.regex); |
|
|
|
|
this->orderedActions << act; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} else { |
|
|
|
|
this->orderedActions.clear(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (cfg.readEntry(Config::showFileContentAction, false)) { |
|
|
|
|
@ -82,6 +82,12 @@ void Pass::reloadConfiguration() |
|
|
|
|
act->setData(Config::showFileContentAction); |
|
|
|
|
this->orderedActions << act; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setDefaultSyntax(Plasma::RunnerSyntax(QString(":q:"), |
|
|
|
|
i18n("Looks for a password matching :q:. Pressing ENTER copies the password to the clipboard."))); |
|
|
|
|
|
|
|
|
|
addSyntax(Plasma::RunnerSyntax(QString("pass :q:"), |
|
|
|
|
i18n("Looks for a password matching :q:. This way you avoid results from other runners"))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Pass::init() |
|
|
|
|
@ -89,42 +95,43 @@ void Pass::init() |
|
|
|
|
reloadConfiguration(); |
|
|
|
|
|
|
|
|
|
this->baseDir = QDir(QDir::homePath() + "/.password-store"); |
|
|
|
|
auto baseDir = getenv("PASSWORD_STORE_DIR"); |
|
|
|
|
if (baseDir != nullptr) { |
|
|
|
|
this->baseDir = QDir(baseDir); |
|
|
|
|
auto _baseDir = getenv("PASSWORD_STORE_DIR"); |
|
|
|
|
if (_baseDir != nullptr) { |
|
|
|
|
this->baseDir = QDir(_baseDir); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this->timeout = 45; |
|
|
|
|
auto timeout = getenv("PASSWORD_STORE_CLIP_TIME"); |
|
|
|
|
if (timeout != nullptr) { |
|
|
|
|
QString str(timeout); |
|
|
|
|
auto _timeout = getenv("PASSWORD_STORE_CLIP_TIME"); |
|
|
|
|
if (_timeout != nullptr) { |
|
|
|
|
QString str(_timeout); |
|
|
|
|
bool ok; |
|
|
|
|
auto timeout = str.toInt(&ok); |
|
|
|
|
auto _timeoutParsed = str.toInt(&ok); |
|
|
|
|
if (ok) { |
|
|
|
|
this->timeout = timeout; |
|
|
|
|
this->timeout = _timeoutParsed; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this->passOtpIdentifier = "totp::"; |
|
|
|
|
auto passOtpIdentifier = getenv("PASSWORD_STORE_OTP_IDENTIFIER"); |
|
|
|
|
if (passOtpIdentifier != nullptr) { |
|
|
|
|
this->passOtpIdentifier = passOtpIdentifier; |
|
|
|
|
auto _passOtpIdentifier = getenv("PASSWORD_STORE_OTP_IDENTIFIER"); |
|
|
|
|
if (_passOtpIdentifier != nullptr) { |
|
|
|
|
this->passOtpIdentifier = _passOtpIdentifier; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
initPasswords(); |
|
|
|
|
|
|
|
|
|
connect(&watcher, SIGNAL(directoryChanged(QString)), this, SLOT(reinitPasswords(QString))); |
|
|
|
|
connect(&watcher, &QFileSystemWatcher::directoryChanged, this, &Pass::reinitPasswords); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Pass::initPasswords() { |
|
|
|
|
void Pass::initPasswords() |
|
|
|
|
{ |
|
|
|
|
passwords.clear(); |
|
|
|
|
|
|
|
|
|
watcher.addPath(this->baseDir.absolutePath()); |
|
|
|
|
QDirIterator it(this->baseDir, QDirIterator::Subdirectories); |
|
|
|
|
while (it.hasNext()) { |
|
|
|
|
it.next(); |
|
|
|
|
auto fileInfo = it.fileInfo(); |
|
|
|
|
if (fileInfo.isFile() && fileInfo.suffix() == "gpg") { |
|
|
|
|
const auto fileInfo = it.fileInfo(); |
|
|
|
|
if (fileInfo.isFile() && fileInfo.suffix() == QLatin1String("gpg")) { |
|
|
|
|
QString password = this->baseDir.relativeFilePath(fileInfo.absoluteFilePath()); |
|
|
|
|
// Remove suffix ".gpg"
|
|
|
|
|
password.chop(4); |
|
|
|
|
@ -135,8 +142,9 @@ void Pass::initPasswords() { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Pass::reinitPasswords(const QString &path) { |
|
|
|
|
Q_UNUSED(path); |
|
|
|
|
void Pass::reinitPasswords(const QString &path) |
|
|
|
|
{ |
|
|
|
|
Q_UNUSED(path) |
|
|
|
|
|
|
|
|
|
lock.lockForWrite(); |
|
|
|
|
initPasswords(); |
|
|
|
|
@ -145,22 +153,26 @@ void Pass::reinitPasswords(const QString &path) { |
|
|
|
|
|
|
|
|
|
void Pass::match(Plasma::RunnerContext &context) |
|
|
|
|
{ |
|
|
|
|
if (!context.isValid()) return; |
|
|
|
|
if (!context.isValid()) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
auto input = context.query(); |
|
|
|
|
// If we use the prefix we want to remove it
|
|
|
|
|
if (input.contains(queryPrefix)) { |
|
|
|
|
input = input.remove(QLatin1String("pass")).simplified(); |
|
|
|
|
} else if (input.count() < 3 && !context.singleRunnerQueryMode()) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
QList<Plasma::QueryMatch> matches; |
|
|
|
|
|
|
|
|
|
lock.lockForRead(); |
|
|
|
|
Q_FOREACH (auto password, passwords) { |
|
|
|
|
QRegularExpression re(".*" + input + ".*", QRegularExpression::CaseInsensitiveOption); |
|
|
|
|
if (re.match(password).hasMatch()) { |
|
|
|
|
for (const auto &password: qAsConst(passwords)) { |
|
|
|
|
if (password.contains(input, Qt::CaseInsensitive)) { |
|
|
|
|
Plasma::QueryMatch match(this); |
|
|
|
|
if (input.length() == password.length()) { |
|
|
|
|
match.setType(Plasma::QueryMatch::ExactMatch); |
|
|
|
|
} else { |
|
|
|
|
match.setType(Plasma::QueryMatch::CompletionMatch); |
|
|
|
|
} |
|
|
|
|
match.setType(input.length() == password.length() ? |
|
|
|
|
Plasma::QueryMatch::ExactMatch : Plasma::QueryMatch::CompletionMatch); |
|
|
|
|
match.setIcon(QIcon::fromTheme("object-locked")); |
|
|
|
|
match.setText(password); |
|
|
|
|
matches.append(match); |
|
|
|
|
@ -176,42 +188,37 @@ void Pass::clip(const QString &msg) |
|
|
|
|
QClipboard *cb = QApplication::clipboard(); |
|
|
|
|
cb->setText(msg); |
|
|
|
|
QTimer::singleShot(timeout * 1000, cb, [cb]() { |
|
|
|
|
cb->clear(); |
|
|
|
|
}); |
|
|
|
|
cb->setText(QString()); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Pass::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) |
|
|
|
|
{ |
|
|
|
|
Q_UNUSED(context); |
|
|
|
|
auto regexp = QRegularExpression("^" + QRegularExpression::escape(this->passOtpIdentifier) + ".*"); |
|
|
|
|
auto isOtp = match.text().split('/').filter(regexp).size() > 0; |
|
|
|
|
const auto regexp = QRegularExpression("^" + QRegularExpression::escape(this->passOtpIdentifier) + ".*"); |
|
|
|
|
const auto isOtp = !match.text().split('/').filter(regexp).isEmpty(); |
|
|
|
|
|
|
|
|
|
QProcess *pass = new QProcess(); |
|
|
|
|
auto *pass = new QProcess(); |
|
|
|
|
QStringList args; |
|
|
|
|
if (isOtp) { |
|
|
|
|
args << "otp" << "show" << match.text(); |
|
|
|
|
} else { |
|
|
|
|
args << "show" << match.text(); |
|
|
|
|
args << "otp"; |
|
|
|
|
} |
|
|
|
|
args << "show" << match.text(); |
|
|
|
|
pass->start("pass", args); |
|
|
|
|
|
|
|
|
|
connect(pass, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), |
|
|
|
|
[=](int exitCode, QProcess::ExitStatus exitStatus) { |
|
|
|
|
Q_UNUSED(exitCode); |
|
|
|
|
Q_UNUSED(exitStatus); |
|
|
|
|
Q_UNUSED(exitStatus) |
|
|
|
|
|
|
|
|
|
if (exitCode == 0) { |
|
|
|
|
|
|
|
|
|
const auto output = pass->readAllStandardOutput(); |
|
|
|
|
|
|
|
|
|
if (match.selectedAction() != nullptr) { |
|
|
|
|
const auto data = match.selectedAction()->data().toString(); |
|
|
|
|
|
|
|
|
|
if (data == Config::showFileContentAction) { |
|
|
|
|
QMessageBox::information(nullptr, match.text(), output); |
|
|
|
|
} else { |
|
|
|
|
QRegularExpression re(data, QRegularExpression::MultilineOption); |
|
|
|
|
auto matchre = re.match(output); |
|
|
|
|
const auto matchre = re.match(output); |
|
|
|
|
|
|
|
|
|
if (matchre.hasMatch()) { |
|
|
|
|
clip(matchre.captured(1)); |
|
|
|
|
@ -225,9 +232,9 @@ void Pass::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &m |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
auto string = QString::fromUtf8(output.data()); |
|
|
|
|
auto lines = string.split('\n', QString::SkipEmptyParts); |
|
|
|
|
if (lines.count() > 0) { |
|
|
|
|
const auto string = QString::fromUtf8(output.data()); |
|
|
|
|
const auto lines = string.split('\n', QString::SkipEmptyParts); |
|
|
|
|
if (!lines.isEmpty()) { |
|
|
|
|
clip(lines[0]); |
|
|
|
|
this->showNotification(match.text()); |
|
|
|
|
} |
|
|
|
|
@ -243,20 +250,16 @@ QList<QAction *> Pass::actionsForMatch(const Plasma::QueryMatch &match) |
|
|
|
|
{ |
|
|
|
|
Q_UNUSED(match) |
|
|
|
|
|
|
|
|
|
if (showActions) |
|
|
|
|
return this->orderedActions; |
|
|
|
|
|
|
|
|
|
return QList<QAction *>(); |
|
|
|
|
return this->orderedActions; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void Pass::showNotification(const QString &text, const QString &actionName /* = "" */) |
|
|
|
|
void Pass::showNotification(const QString &text, const QString &actionName) |
|
|
|
|
{ |
|
|
|
|
QString msgPrefix = actionName.isEmpty() ? "":actionName + i18n(" of "); |
|
|
|
|
QString msg = i18n("Password %1 copied to clipboard for %2 seconds", text, timeout); |
|
|
|
|
auto notification = KNotification::event("password-unlocked", "Pass", msgPrefix + msg, |
|
|
|
|
"object-unlocked", nullptr, KNotification::CloseOnTimeout, |
|
|
|
|
"krunner_pass"); |
|
|
|
|
QTimer::singleShot(timeout * 1000, notification, SLOT(quit)); |
|
|
|
|
const QString msgPrefix = actionName.isEmpty() ? "" : actionName + i18n(" of "); |
|
|
|
|
const QString msg = i18n("Password %1 copied to clipboard for %2 seconds", text, timeout); |
|
|
|
|
KNotification::event("password-unlocked", "Pass", msgPrefix + msg, |
|
|
|
|
"object-unlocked", nullptr, KNotification::CloseOnTimeout, |
|
|
|
|
"krunner_pass"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
K_EXPORT_PLASMA_RUNNER(pass, Pass) |
|
|
|
|
|