Support for triggering Global Auto-Type from browser extension

This commit is contained in:
varjolintu 2021-10-10 14:49:25 +03:00 committed by Jonathan White
parent be6835e42f
commit c7cdce6e33
15 changed files with 164 additions and 62 deletions

View File

@ -164,11 +164,14 @@ void AutoType::loadPlugin(const QString& pluginPath)
if (m_plugin) { if (m_plugin) {
if (m_plugin->isAvailable()) { if (m_plugin->isAvailable()) {
m_executor = m_plugin->createExecutor(); m_executor = m_plugin->createExecutor();
connect(osUtils, &OSUtilsBase::globalShortcutTriggered, this, [this](const QString& name) { connect(osUtils,
if (name == "autotype") { &OSUtilsBase::globalShortcutTriggered,
startGlobalAutoType(); this,
} [this](const QString& name, const QString& initialSearch) {
}); if (name == "autotype") {
startGlobalAutoType(initialSearch);
}
});
} else { } else {
unloadPlugin(); unloadPlugin();
} }
@ -359,7 +362,7 @@ void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& se
executeAutoTypeActions(entry, hideWindow, sequence); executeAutoTypeActions(entry, hideWindow, sequence);
} }
void AutoType::startGlobalAutoType() void AutoType::startGlobalAutoType(const QString& search)
{ {
// Never Auto-Type into KeePassXC itself // Never Auto-Type into KeePassXC itself
if (qApp->focusWindow()) { if (qApp->focusWindow()) {
@ -382,6 +385,7 @@ void AutoType::startGlobalAutoType()
tr("KeePassXC requires the Accessibility and Screen Recorder permission in order to perform global " tr("KeePassXC requires the Accessibility and Screen Recorder permission in order to perform global "
"Auto-Type. Screen Recording is necessary to use the window title to find entries. If you " "Auto-Type. Screen Recording is necessary to use the window title to find entries. If you "
"already granted permission, you may have to restart KeePassXC.")); "already granted permission, you may have to restart KeePassXC."));
qDebug() << "Oh noes macOS.";
return; return;
} }
} }
@ -397,14 +401,14 @@ void AutoType::startGlobalAutoType()
} }
#endif #endif
emit globalAutoTypeTriggered(); emit globalAutoTypeTriggered(search);
} }
/** /**
* Global Autotype entry-point function * Global Autotype entry-point function
* Perform global Auto-Type on the active window * Perform global Auto-Type on the active window
*/ */
void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList) void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList, const QString& search)
{ {
if (!m_plugin) { if (!m_plugin) {
return; return;
@ -449,6 +453,10 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
auto* selectDialog = new AutoTypeSelectDialog(); auto* selectDialog = new AutoTypeSelectDialog();
selectDialog->setMatches(matchList, dbList); selectDialog->setMatches(matchList, dbList);
if (!search.isEmpty()) {
selectDialog->setSearchString(search);
}
connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject); connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) { connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) {
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal); executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);

View File

@ -52,16 +52,16 @@ public:
static void createTestInstance(); static void createTestInstance();
public slots: public slots:
void performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList); void performGlobalAutoType(const QList<QSharedPointer<Database>>& dbList, const QString& search = {});
void raiseWindow(); void raiseWindow();
signals: signals:
void globalAutoTypeTriggered(); void globalAutoTypeTriggered(const QString& search);
void autotypePerformed(); void autotypePerformed();
void autotypeRejected(); void autotypeRejected();
private slots: private slots:
void startGlobalAutoType(); void startGlobalAutoType(const QString& search);
void unloadPlugin(); void unloadPlugin();
private: private:

View File

@ -102,6 +102,12 @@ void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches, const
m_ui->searchCheckBox->setChecked(m_matches.isEmpty()); m_ui->searchCheckBox->setChecked(m_matches.isEmpty());
} }
void AutoTypeSelectDialog::setSearchString(const QString& search)
{
m_ui->search->setText(search);
m_ui->searchCheckBox->setChecked(true);
}
void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match) void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
{ {
if (match.first) { if (match.first) {

View File

@ -40,6 +40,7 @@ public:
~AutoTypeSelectDialog() override; ~AutoTypeSelectDialog() override;
void setMatches(const QList<AutoTypeMatch>& matchList, const QList<QSharedPointer<Database>>& dbs); void setMatches(const QList<AutoTypeMatch>& matchList, const QList<QSharedPointer<Database>>& dbs);
void setSearchString(const QString& search);
signals: signals:
void matchActivated(AutoTypeMatch match); void matchActivated(AutoTypeMatch match);

View File

@ -57,6 +57,8 @@ namespace
}; };
} }
const int BrowserAction::MaxUrlLength = 256;
QJsonObject BrowserAction::processClientMessage(const QJsonObject& json) QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
{ {
if (json.isEmpty()) { if (json.isEmpty()) {
@ -65,7 +67,7 @@ QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
bool triggerUnlock = false; bool triggerUnlock = false;
const QString trigger = json.value("triggerUnlock").toString(); const QString trigger = json.value("triggerUnlock").toString();
if (!trigger.isEmpty() && trigger.compare(TRUE_STR, Qt::CaseSensitive) == 0) { if (!trigger.isEmpty() && trigger.compare(TRUE_STR) == 0) {
triggerUnlock = true; triggerUnlock = true;
} }
@ -74,7 +76,8 @@ QJsonObject BrowserAction::processClientMessage(const QJsonObject& json)
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
} }
if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !browserService()->isDatabaseOpened()) { if (action.compare("change-public-keys") != 0 && action.compare("request-autotype") != 0
&& !browserService()->isDatabaseOpened()) {
if (m_clientPublicKey.isEmpty()) { if (m_clientPublicKey.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED);
} else if (!browserService()->openDatabase(triggerUnlock)) { } else if (!browserService()->openDatabase(triggerUnlock)) {
@ -92,30 +95,32 @@ QJsonObject BrowserAction::handleAction(const QJsonObject& json)
{ {
QString action = json.value("action").toString(); QString action = json.value("action").toString();
if (action.compare("change-public-keys", Qt::CaseSensitive) == 0) { if (action.compare("change-public-keys") == 0) {
return handleChangePublicKeys(json, action); return handleChangePublicKeys(json, action);
} else if (action.compare("get-databasehash", Qt::CaseSensitive) == 0) { } else if (action.compare("get-databasehash") == 0) {
return handleGetDatabaseHash(json, action); return handleGetDatabaseHash(json, action);
} else if (action.compare("associate", Qt::CaseSensitive) == 0) { } else if (action.compare("associate") == 0) {
return handleAssociate(json, action); return handleAssociate(json, action);
} else if (action.compare("test-associate", Qt::CaseSensitive) == 0) { } else if (action.compare("test-associate") == 0) {
return handleTestAssociate(json, action); return handleTestAssociate(json, action);
} else if (action.compare("get-logins", Qt::CaseSensitive) == 0) { } else if (action.compare("get-logins") == 0) {
return handleGetLogins(json, action); return handleGetLogins(json, action);
} else if (action.compare("generate-password", Qt::CaseSensitive) == 0) { } else if (action.compare("generate-password") == 0) {
return handleGeneratePassword(json, action); return handleGeneratePassword(json, action);
} else if (action.compare("set-login", Qt::CaseSensitive) == 0) { } else if (action.compare("set-login") == 0) {
return handleSetLogin(json, action); return handleSetLogin(json, action);
} else if (action.compare("lock-database", Qt::CaseSensitive) == 0) { } else if (action.compare("lock-database") == 0) {
return handleLockDatabase(json, action); return handleLockDatabase(json, action);
} else if (action.compare("get-database-groups", Qt::CaseSensitive) == 0) { } else if (action.compare("get-database-groups") == 0) {
return handleGetDatabaseGroups(json, action); return handleGetDatabaseGroups(json, action);
} else if (action.compare("create-new-group", Qt::CaseSensitive) == 0) { } else if (action.compare("create-new-group") == 0) {
return handleCreateNewGroup(json, action); return handleCreateNewGroup(json, action);
} else if (action.compare("get-totp", Qt::CaseSensitive) == 0) { } else if (action.compare("get-totp") == 0) {
return handleGetTotp(json, action); return handleGetTotp(json, action);
} else if (action.compare("delete-entry", Qt::CaseSensitive) == 0) { } else if (action.compare("delete-entry") == 0) {
return handleDeleteEntry(json, action); return handleDeleteEntry(json, action);
} else if (action.compare("request-autotype") == 0) {
return handleGlobalAutoType(json, action);
} }
// Action was not recognized // Action was not recognized
@ -169,7 +174,7 @@ QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (!command.isEmpty() && command.compare("get-databasehash", Qt::CaseSensitive) == 0) { if (!command.isEmpty() && command.compare("get-databasehash") == 0) {
const QString newNonce = incrementNonce(nonce); const QString newNonce = incrementNonce(nonce);
QJsonObject message = buildMessage(newNonce); QJsonObject message = buildMessage(newNonce);
@ -206,7 +211,7 @@ QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
} }
if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { if (key.compare(m_clientPublicKey) == 0) {
// Check for identification key. If it's not found, ensure backwards compatibility and use the current public // Check for identification key. If it's not found, ensure backwards compatibility and use the current public
// key // key
const QString idKey = decrypted.value("idKey").toString(); const QString idKey = decrypted.value("idKey").toString();
@ -245,7 +250,7 @@ QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QS
} }
const QString key = browserService()->getKey(id); const QString key = browserService()->getKey(id);
if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { if (key.isEmpty() || key.compare(responseKey) != 0) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
} }
@ -290,7 +295,7 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
const QString id = decrypted.value("id").toString(); const QString id = decrypted.value("id").toString();
const QString formUrl = decrypted.value("submitUrl").toString(); const QString formUrl = decrypted.value("submitUrl").toString();
const QString auth = decrypted.value("httpAuth").toString(); const QString auth = decrypted.value("httpAuth").toString();
const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0; const bool httpAuth = auth.compare(TRUE_STR) == 0;
const QJsonArray users = browserService()->findMatchingEntries(id, siteUrl, formUrl, "", keyList, httpAuth); const QJsonArray users = browserService()->findMatchingEntries(id, siteUrl, formUrl, "", keyList, httpAuth);
if (users.isEmpty()) { if (users.isEmpty()) {
@ -398,7 +403,7 @@ QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QSt
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { if (!command.isEmpty() && command.compare("lock-database") == 0) {
browserService()->lockDatabase(); browserService()->lockDatabase();
const QString newNonce = incrementNonce(nonce); const QString newNonce = incrementNonce(nonce);
@ -426,7 +431,7 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("get-database-groups", Qt::CaseSensitive) != 0) { if (command.isEmpty() || command.compare("get-database-groups") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
} }
@ -459,7 +464,7 @@ QJsonObject BrowserAction::handleCreateNewGroup(const QJsonObject& json, const Q
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("create-new-group", Qt::CaseSensitive) != 0) { if (command.isEmpty() || command.compare("create-new-group") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
} }
@ -493,7 +498,7 @@ QJsonObject BrowserAction::handleGetTotp(const QJsonObject& json, const QString&
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("get-totp", Qt::CaseSensitive) != 0) { if (command.isEmpty() || command.compare("get-totp") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
} }
@ -527,7 +532,7 @@ QJsonObject BrowserAction::handleDeleteEntry(const QJsonObject& json, const QStr
} }
QString command = decrypted.value("action").toString(); QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("delete-entry", Qt::CaseSensitive) != 0) { if (command.isEmpty() || command.compare("delete-entry") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
} }
@ -545,6 +550,33 @@ QJsonObject BrowserAction::handleDeleteEntry(const QJsonObject& json, const QStr
return buildResponse(action, message, newNonce); return buildResponse(action, message, newNonce);
} }
QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const QString& action)
{
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();
const QJsonObject decrypted = decryptMessage(encrypted, nonce);
if (decrypted.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
QString command = decrypted.value("action").toString();
if (command.isEmpty() || command.compare("request-autotype") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto topLevelDomain = decrypted.value("search").toString();
if (topLevelDomain.length() > BrowserAction::MaxUrlLength) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
browserService()->requestGlobalAutoType(topLevelDomain);
const QString newNonce = incrementNonce(nonce);
QJsonObject message = buildMessage(newNonce);
return buildResponse(action, message, newNonce);
}
QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const
{ {
QJsonObject response; QJsonObject response;

View File

@ -44,6 +44,7 @@ private:
QJsonObject handleCreateNewGroup(const QJsonObject& json, const QString& action); QJsonObject handleCreateNewGroup(const QJsonObject& json, const QString& action);
QJsonObject handleGetTotp(const QJsonObject& json, const QString& action); QJsonObject handleGetTotp(const QJsonObject& json, const QString& action);
QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action); QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action);
QJsonObject handleGlobalAutoType(const QJsonObject& json, const QString& action);
QJsonObject buildMessage(const QString& nonce) const; QJsonObject buildMessage(const QString& nonce) const;
QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce); QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce);
@ -63,6 +64,8 @@ private:
QString incrementNonce(const QString& nonce); QString incrementNonce(const QString& nonce);
private: private:
static const int MaxUrlLength;
QString m_clientPublicKey; QString m_clientPublicKey;
QString m_publicKey; QString m_publicKey;
QString m_secretKey; QString m_secretKey;

View File

@ -18,7 +18,6 @@
*/ */
#include "BrowserService.h" #include "BrowserService.h"
#include "BrowserAccessControlDialog.h" #include "BrowserAccessControlDialog.h"
#include "BrowserAction.h" #include "BrowserAction.h"
#include "BrowserEntryConfig.h" #include "BrowserEntryConfig.h"
@ -28,6 +27,7 @@
#include "core/Tools.h" #include "core/Tools.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h" #include "gui/osutils/macutils/MacUtils.h"
#endif #endif
@ -761,6 +761,11 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db)
} }
} }
void BrowserService::requestGlobalAutoType(const QString& search)
{
emit osUtils->globalShortcutTriggered("autotype", search);
}
QList<Entry*> QList<Entry*>
BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr) BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr)
{ {
@ -1012,6 +1017,12 @@ bool BrowserService::schemeFound(const QString& url)
return !address.scheme().isEmpty(); return !address.scheme().isEmpty();
} }
bool BrowserService::isIpAddress(const QString& host) const
{
QHostAddress address(host);
return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol;
}
bool BrowserService::removeFirstDomain(QString& hostname) bool BrowserService::removeFirstDomain(QString& hostname)
{ {
int pos = hostname.indexOf("."); int pos = hostname.indexOf(".");
@ -1088,7 +1099,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
} }
// Match the base domain // Match the base domain
if (baseDomain(siteQUrl.host()) != baseDomain(entryQUrl.host())) { if (getTopLevelDomainFromUrl(siteQUrl.host()) != getTopLevelDomainFromUrl(entryQUrl.host())) {
return false; return false;
} }
@ -1105,15 +1116,14 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
* *
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
*/ */
QString BrowserService::baseDomain(const QString& hostname) const QString BrowserService::getTopLevelDomainFromUrl(const QString& url) const
{ {
QUrl qurl = QUrl::fromUserInput(hostname); QUrl qurl = QUrl::fromUserInput(url);
QString host = qurl.host(); QString host = qurl.host();
// If the hostname is an IP address, return it directly // If the hostname is an IP address, return it directly
QHostAddress hostAddress(hostname); if (isIpAddress(host)) {
if (!hostAddress.isNull()) { return host;
return hostname;
} }
if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) { if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) {

View File

@ -80,6 +80,7 @@ public:
const StringPairList& keyList, const StringPairList& keyList,
const bool httpAuth = false); const bool httpAuth = false);
void requestGlobalAutoType(const QString& search);
static void convertAttributesToCustomData(QSharedPointer<Database> db); static void convertAttributesToCustomData(QSharedPointer<Database> db);
static const QString KEEPASSXCBROWSER_NAME; static const QString KEEPASSXCBROWSER_NAME;
@ -132,9 +133,11 @@ private:
Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {}); Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {});
int sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr); int sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr);
bool schemeFound(const QString& url); bool schemeFound(const QString& url);
bool isIpAddress(const QString& host) const;
bool removeFirstDomain(QString& hostname); bool removeFirstDomain(QString& hostname);
bool handleEntry(Entry* entry, const QString& url, const QString& submitUrl); bool handleEntry(Entry* entry, const QString& url, const QString& submitUrl);
bool handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr); bool handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr);
QString getTopLevelDomainFromUrl(const QString& url) const;
QString baseDomain(const QString& hostname) const; QString baseDomain(const QString& hostname) const;
QSharedPointer<Database> getDatabase(); QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase(); QSharedPointer<Database> selectedDatabase();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -47,7 +47,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
connect(this, SIGNAL(currentChanged(int)), SLOT(emitActiveDatabaseChanged())); connect(this, SIGNAL(currentChanged(int)), SLOT(emitActiveDatabaseChanged()));
connect(this, SIGNAL(activeDatabaseChanged(DatabaseWidget*)), connect(this, SIGNAL(activeDatabaseChanged(DatabaseWidget*)),
m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*))); m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*)));
connect(autoType(), SIGNAL(globalAutoTypeTriggered()), SLOT(performGlobalAutoType())); connect(autoType(), SIGNAL(globalAutoTypeTriggered(const QString&)), SLOT(performGlobalAutoType(const QString&)));
connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase())); connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase()));
connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase())); connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase()));
connect(m_databaseOpenDialog.data(), &DatabaseOpenDialog::dialogFinished, connect(m_databaseOpenDialog.data(), &DatabaseOpenDialog::dialogFinished,
@ -790,28 +790,30 @@ void DatabaseTabWidget::emitDatabaseLockChanged()
} }
} }
void DatabaseTabWidget::performGlobalAutoType() void DatabaseTabWidget::performGlobalAutoType(const QString& search)
{ {
auto currentDbWidget = currentDatabaseWidget(); auto currentDbWidget = currentDatabaseWidget();
if (!currentDbWidget) { if (!currentDbWidget) {
// no open databases, nothing to do // No open databases, nothing to do
return; return;
} else if (currentDbWidget->isLocked()) { } else if (currentDbWidget->isLocked()) {
// Current database tab is locked, match behavior of browser unlock - prompt with // Current database tab is locked, match behavior of browser unlock - prompt with
// the unlock dialog even if there are additional unlocked open database tabs. // the unlock dialog even if there are additional unlocked open database tabs.
currentDbWidget->setSearchStringForAutoType(search);
unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::AutoType); unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::AutoType);
} else { } else {
// current database is unlocked, use it for AutoType along with any other unlocked databases // Current database is unlocked, use it for AutoType along with any other unlocked databases
QList<QSharedPointer<Database>> unlockedDatabases; QList<QSharedPointer<Database>> unlockedDatabases;
for (int i = 0, c = count(); i < c; ++i) { for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i); auto* dbWidget = databaseWidgetFromIndex(i);
if (!dbWidget->isLocked()) { if (!dbWidget->isLocked()) {
dbWidget->setSearchStringForAutoType(search);
unlockedDatabases.append(dbWidget->database()); unlockedDatabases.append(dbWidget->database());
} }
} }
Q_ASSERT(!unlockedDatabases.isEmpty()); Q_ASSERT(!unlockedDatabases.isEmpty());
autoType()->performGlobalAutoType(unlockedDatabases); autoType()->performGlobalAutoType(unlockedDatabases, search);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -80,7 +80,7 @@ public slots:
void showDatabaseSecurity(); void showDatabaseSecurity();
void showDatabaseReports(); void showDatabaseReports();
void showDatabaseSettings(); void showDatabaseSettings();
void performGlobalAutoType(); void performGlobalAutoType(const QString& search);
void performBrowserUnlock(); void performBrowserUnlock();
signals: signals:

View File

@ -186,7 +186,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged())); connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
connect(this, SIGNAL(requestGlobalAutoType()), parent, SLOT(performGlobalAutoType())); connect(this, SIGNAL(requestGlobalAutoType(const QString&)), parent, SLOT(performGlobalAutoType(const QString&)));
// clang-format on // clang-format on
connectDatabaseSignals(); connectDatabaseSignals();
@ -309,6 +309,11 @@ void DatabaseWidget::setPreviewSplitterSizes(const QList<int>& sizes)
m_previewSplitter->setSizes(sizes); m_previewSplitter->setSizes(sizes);
} }
void DatabaseWidget::setSearchStringForAutoType(const QString& search)
{
m_searchStringForAutoType = search;
}
/** /**
* Get current view state of entry view * Get current view state of entry view
*/ */
@ -1118,7 +1123,7 @@ void DatabaseWidget::unlockDatabase(bool accepted)
// Rather than starting AutoType directly for this database, signal the parent DatabaseTabWidget to // Rather than starting AutoType directly for this database, signal the parent DatabaseTabWidget to
// restart AutoType now that this database is unlocked, so that other open+unlocked databases // restart AutoType now that this database is unlocked, so that other open+unlocked databases
// can be included in the search. // can be included in the search.
emit requestGlobalAutoType(); emit requestGlobalAutoType(m_searchStringForAutoType);
} }
} }

View File

@ -121,6 +121,7 @@ public:
void setMainSplitterSizes(const QList<int>& sizes); void setMainSplitterSizes(const QList<int>& sizes);
QList<int> previewSplitterSizes() const; QList<int> previewSplitterSizes() const;
void setPreviewSplitterSizes(const QList<int>& sizes); void setPreviewSplitterSizes(const QList<int>& sizes);
void setSearchStringForAutoType(const QString& search);
signals: signals:
// relayed Database signals // relayed Database signals
@ -151,7 +152,7 @@ signals:
void previewSplitterSizesChanged(); void previewSplitterSizesChanged();
void entryViewStateChanged(); void entryViewStateChanged();
void clearSearch(); void clearSearch();
void requestGlobalAutoType(); void requestGlobalAutoType(const QString& search);
public slots: public slots:
bool lock(); bool lock();
@ -297,6 +298,9 @@ private:
// Autoreload // Autoreload
bool m_blockAutoSave; bool m_blockAutoSave;
// Auto-Type related
QString m_searchStringForAutoType;
}; };
#endif // KEEPASSX_DATABASEWIDGET_H #endif // KEEPASSX_DATABASEWIDGET_H

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -68,7 +68,7 @@ public:
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const; virtual bool setPreventScreenCapture(QWindow* window, bool allow) const;
signals: signals:
void globalShortcutTriggered(const QString& name); void globalShortcutTriggered(const QString& name, const QString& search = {});
/** /**
* Indicates platform UI theme change (light mode to dark mode). * Indicates platform UI theme change (light mode to dark mode).

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -112,22 +112,49 @@ void TestBrowser::testIncrementNonce()
/** /**
* Tests for BrowserService * Tests for BrowserService
*/ */
void TestBrowser::testBaseDomain() void TestBrowser::testTopLevelDomain()
{ {
QString url1 = "https://another.example.co.uk"; QString url1 = "https://another.example.co.uk";
QString url2 = "https://www.example.com"; QString url2 = "https://www.example.com";
QString url3 = "http://test.net"; QString url3 = "http://test.net";
QString url4 = "http://so.many.subdomains.co.jp"; QString url4 = "http://so.many.subdomains.co.jp";
QString url5 = "https://192.168.0.1";
QString url6 = "https://192.168.0.1:8000";
QString res1 = m_browserService->baseDomain(url1); QString res1 = m_browserService->getTopLevelDomainFromUrl(url1);
QString res2 = m_browserService->baseDomain(url2); QString res2 = m_browserService->getTopLevelDomainFromUrl(url2);
QString res3 = m_browserService->baseDomain(url3); QString res3 = m_browserService->getTopLevelDomainFromUrl(url3);
QString res4 = m_browserService->baseDomain(url4); QString res4 = m_browserService->getTopLevelDomainFromUrl(url4);
QString res5 = m_browserService->getTopLevelDomainFromUrl(url5);
QString res6 = m_browserService->getTopLevelDomainFromUrl(url6);
QCOMPARE(res1, QString("example.co.uk")); QCOMPARE(res1, QString("example.co.uk"));
QCOMPARE(res2, QString("example.com")); QCOMPARE(res2, QString("example.com"));
QCOMPARE(res3, QString("test.net")); QCOMPARE(res3, QString("test.net"));
QCOMPARE(res4, QString("subdomains.co.jp")); QCOMPARE(res4, QString("subdomains.co.jp"));
QCOMPARE(res5, QString("192.168.0.1"));
QCOMPARE(res6, QString("192.168.0.1"));
}
void TestBrowser::testIsIpAddress()
{
auto host1 = "example.com"; // Not valid
auto host2 = "192.168.0.1";
auto host3 = "278.21.2.0"; // Not valid
auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
auto host5 = "2001:db8:0:1:1:1:1:1";
auto host6 = "fe80::1ff:fe23:4567:890a";
auto host7 = "2001:20::1";
auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid
QVERIFY(!m_browserService->isIpAddress(host1));
QVERIFY(m_browserService->isIpAddress(host2));
QVERIFY(!m_browserService->isIpAddress(host3));
QVERIFY(m_browserService->isIpAddress(host4));
QVERIFY(m_browserService->isIpAddress(host5));
QVERIFY(m_browserService->isIpAddress(host6));
QVERIFY(m_browserService->isIpAddress(host7));
QVERIFY(!m_browserService->isIpAddress(host8));
} }
void TestBrowser::testSortPriority() void TestBrowser::testSortPriority()

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -37,7 +37,8 @@ private slots:
void testGetBase64FromKey(); void testGetBase64FromKey();
void testIncrementNonce(); void testIncrementNonce();
void testBaseDomain(); void testTopLevelDomain();
void testIsIpAddress();
void testSortPriority(); void testSortPriority();
void testSortPriority_data(); void testSortPriority_data();
void testSearchEntries(); void testSearchEntries();