/****************************************************************************** * Copyright (C) 2016 by Dominik Schreiber * * * * This library is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published * * by the Free Software Foundation; either version 3 of the License or (at * * your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Library General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this library; see the file LICENSE. * * If not, see . * *****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include "pass.h" #include "config.h" using namespace std; Pass::Pass(QObject *parent, const QVariantList &args) : Plasma::AbstractRunner(parent, args) { Q_UNUSED(args); // General runner configuration setObjectName(QString("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() {} void Pass::reloadConfiguration() { actions().clear(); orderedActions.clear(); KConfigGroup cfg = config(); this->showActions = cfg.readEntry(Config::showActions, false); if (showActions) { auto configActions = cfg.group(Config::Group::Actions); // Create actions for every additional field auto groups = configActions.groupList(); Q_FOREACH (auto name, groups) { 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); act->setData(passAction.regex); this->orderedActions << act; } } if (cfg.readEntry(Config::showFileContentAction, false)) { QAction *act = addAction(Config::showFileContentAction, QIcon::fromTheme("document-new"), i18n("Show password file contents")); act->setData(Config::showFileContentAction); this->orderedActions << act; } } void Pass::init() { reloadConfiguration(); this->baseDir = QDir(QDir::homePath() + "/.password-store"); 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); bool ok; auto timeout = str.toInt(&ok); if (ok) { this->timeout = timeout; } } this->passOtpIdentifier = "totp::"; auto passOtpIdentifier = getenv("PASSWORD_STORE_OTP_IDENTIFIER"); if (passOtpIdentifier != nullptr) { this->passOtpIdentifier = passOtpIdentifier; } initPasswords(); connect(&watcher, SIGNAL(directoryChanged(QString)), this, SLOT(reinitPasswords(QString))); } 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") { QString password = this->baseDir.relativeFilePath(fileInfo.absoluteFilePath()); // Remove suffix ".gpg" password.chop(4); passwords.append(password); } else if (fileInfo.isDir() && it.fileName() != "." && it.fileName() != "..") { watcher.addPath(it.filePath()); } } } void Pass::reinitPasswords(const QString &path) { Q_UNUSED(path); lock.lockForWrite(); initPasswords(); lock.unlock(); } void Pass::match(Plasma::RunnerContext &context) { if (!context.isValid()) return; auto input = context.query(); QList matches; lock.lockForRead(); Q_FOREACH (auto password, passwords) { QRegularExpression re(".*" + input + ".*", QRegularExpression::CaseInsensitiveOption); if (re.match(password).hasMatch()) { Plasma::QueryMatch match(this); if (input.length() == password.length()) { match.setType(Plasma::QueryMatch::ExactMatch); } else { match.setType(Plasma::QueryMatch::CompletionMatch); } match.setIcon(QIcon::fromTheme("object-locked")); match.setText(password); matches.append(match); } } lock.unlock(); context.addMatches(matches); } void Pass::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) { Q_UNUSED(context); if (match.selectedAction() != nullptr) { const auto data = match.selectedAction()->data().toString(); QProcess *passProcess = new QProcess(); passProcess->start("pass", QStringList() << match.text()); connect(passProcess, static_cast(&QProcess::finished), [=](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitCode) Q_UNUSED(exitStatus) const auto output = passProcess->readAllStandardOutput(); if (data == Config::showFileContentAction) { QMessageBox::information(nullptr, match.text(), output); } else { QRegularExpression re(data, QRegularExpression::MultilineOption); auto matchre = re.match(output); if (matchre.hasMatch()) { QClipboard *cb = QApplication::clipboard(); cb->setText(matchre.captured(1)); this->showNotification(match.text(), match.selectedAction()->text()); QTimer::singleShot(timeout * 1000, cb, [cb]() { cb->clear(); }); } else { // Show some information to understand what went wrong. qInfo() << "Regexp: " << data; qInfo() << "Is regexp valid? " << re.isValid(); qInfo() << "The file: " << match.text(); // qInfo() << "Content: " << output; } } passProcess->close(); passProcess->deleteLater(); }); } else { auto regexp = QRegularExpression("^" + QRegularExpression::escape(this->passOtpIdentifier) + ".*"); auto isOtp = match.text().split('/').filter(regexp).size() > 0; auto ret = isOtp ? QProcess::execute(QString("pass"), QStringList() << "otp" << "-c" << match.text()) : QProcess::execute(QString("pass"), QStringList() << "-c" << match.text()); if (ret == 0) { this->showNotification(match.text()); } } } QList Pass::actionsForMatch(const Plasma::QueryMatch &match) { Q_UNUSED(match) if (showActions) return this->orderedActions; return QList(); } 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)); } K_EXPORT_PLASMA_RUNNER(pass, Pass) #include "pass.moc"