mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-24 00:19:57 -05:00

* The main window doesn't hide properly during unlock sequence if it is in the background (ie, not minimized and not hidden to tray). This change makes sure the window hides after interaction on all platforms.
1352 lines
42 KiB
C++
1352 lines
42 KiB
C++
/*
|
|
* Copyright (C) 2013 Francois Ferrand
|
|
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
|
|
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
|
|
*
|
|
* 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
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "BrowserService.h"
|
|
|
|
#include "BrowserAccessControlDialog.h"
|
|
#include "BrowserAction.h"
|
|
#include "BrowserEntryConfig.h"
|
|
#include "BrowserEntrySaveDialog.h"
|
|
#include "BrowserHost.h"
|
|
#include "BrowserSettings.h"
|
|
#include "core/Tools.h"
|
|
#include "gui/MainWindow.h"
|
|
#include "gui/MessageBox.h"
|
|
#ifdef Q_OS_MACOS
|
|
#include "gui/osutils/macutils/MacUtils.h"
|
|
#endif
|
|
|
|
#include <QCheckBox>
|
|
#include <QCryptographicHash>
|
|
#include <QHostAddress>
|
|
#include <QInputDialog>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QListWidget>
|
|
#include <QProgressDialog>
|
|
#include <QUrl>
|
|
|
|
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
|
|
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
|
|
static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
|
|
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
|
|
// These are for the settings and password conversion
|
|
static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
|
|
static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
|
|
// Extra entry related options saved in custom data
|
|
const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit");
|
|
const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
|
|
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
|
|
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
|
|
// Multiple URL's
|
|
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
|
|
|
|
Q_GLOBAL_STATIC(BrowserService, s_browserService);
|
|
|
|
BrowserService::BrowserService()
|
|
: QObject()
|
|
, m_browserHost(new BrowserHost)
|
|
, m_dialogActive(false)
|
|
, m_bringToFrontRequested(false)
|
|
, m_prevWindowState(WindowState::Normal)
|
|
, m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
|
|
{
|
|
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage);
|
|
connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked);
|
|
connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
|
|
connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
|
|
|
|
setEnabled(browserSettings()->isEnabled());
|
|
}
|
|
|
|
BrowserService* BrowserService::instance()
|
|
{
|
|
return s_browserService;
|
|
}
|
|
|
|
void BrowserService::setEnabled(bool enabled)
|
|
{
|
|
if (enabled) {
|
|
// Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts
|
|
if (browserSettings()->updateBinaryPath()) {
|
|
browserSettings()->updateBinaryPaths();
|
|
}
|
|
|
|
m_browserHost->start();
|
|
} else {
|
|
m_browserHost->stop();
|
|
}
|
|
}
|
|
|
|
bool BrowserService::isDatabaseOpened() const
|
|
{
|
|
if (m_currentDatabaseWidget) {
|
|
return !m_currentDatabaseWidget->isLocked();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool BrowserService::openDatabase(bool triggerUnlock)
|
|
{
|
|
if (!browserSettings()->unlockDatabase()) {
|
|
return false;
|
|
}
|
|
|
|
if (m_currentDatabaseWidget && !m_currentDatabaseWidget->isLocked()) {
|
|
return true;
|
|
}
|
|
|
|
if (triggerUnlock) {
|
|
m_bringToFrontRequested = true;
|
|
updateWindowState();
|
|
emit requestUnlock();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void BrowserService::lockDatabase()
|
|
{
|
|
if (m_currentDatabaseWidget) {
|
|
m_currentDatabaseWidget->lock();
|
|
}
|
|
}
|
|
|
|
QString BrowserService::getDatabaseHash(bool legacy)
|
|
{
|
|
if (legacy) {
|
|
return QCryptographicHash::hash(
|
|
(browserService()->getDatabaseRootUuid() + browserService()->getDatabaseRecycleBinUuid()).toUtf8(),
|
|
QCryptographicHash::Sha256)
|
|
.toHex();
|
|
}
|
|
return QCryptographicHash::hash(getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex();
|
|
}
|
|
|
|
QString BrowserService::getDatabaseRootUuid()
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
Group* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return {};
|
|
}
|
|
|
|
return rootGroup->uuidToHex();
|
|
}
|
|
|
|
QString BrowserService::getDatabaseRecycleBinUuid()
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
Group* recycleBin = db->metadata()->recycleBin();
|
|
if (!recycleBin) {
|
|
return {};
|
|
}
|
|
return recycleBin->uuidToHex();
|
|
}
|
|
|
|
QJsonArray BrowserService::getChildrenFromGroup(Group* group)
|
|
{
|
|
QJsonArray groupList;
|
|
|
|
if (!group) {
|
|
return groupList;
|
|
}
|
|
|
|
for (const auto& c : group->children()) {
|
|
if (c == group->database()->metadata()->recycleBin()) {
|
|
continue;
|
|
}
|
|
|
|
QJsonObject jsonGroup;
|
|
jsonGroup["name"] = c->name();
|
|
jsonGroup["uuid"] = Tools::uuidToHex(c->uuid());
|
|
jsonGroup["children"] = getChildrenFromGroup(c);
|
|
groupList.push_back(jsonGroup);
|
|
}
|
|
return groupList;
|
|
}
|
|
|
|
QJsonObject BrowserService::getDatabaseGroups()
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
Group* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return {};
|
|
}
|
|
|
|
QJsonObject root;
|
|
root["name"] = rootGroup->name();
|
|
root["uuid"] = Tools::uuidToHex(rootGroup->uuid());
|
|
root["children"] = getChildrenFromGroup(rootGroup);
|
|
|
|
QJsonArray groups;
|
|
groups.push_back(root);
|
|
|
|
QJsonObject result;
|
|
result["groups"] = groups;
|
|
|
|
return result;
|
|
}
|
|
|
|
QJsonObject BrowserService::createNewGroup(const QString& groupName)
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
Group* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return {};
|
|
}
|
|
|
|
auto group = rootGroup->findGroupByPath(groupName);
|
|
|
|
// Group already exists
|
|
if (group) {
|
|
QJsonObject result;
|
|
result["name"] = group->name();
|
|
result["uuid"] = Tools::uuidToHex(group->uuid());
|
|
return result;
|
|
}
|
|
|
|
auto dialogResult = MessageBox::warning(nullptr,
|
|
tr("KeePassXC: Create a new group"),
|
|
tr("A request for creating a new group \"%1\" has been received.\n"
|
|
"Do you want to create this group?\n")
|
|
.arg(groupName),
|
|
MessageBox::Yes | MessageBox::No);
|
|
|
|
if (dialogResult != MessageBox::Yes) {
|
|
return {};
|
|
}
|
|
|
|
QString name, uuid;
|
|
Group* previousGroup = rootGroup;
|
|
auto groups = groupName.split("/");
|
|
|
|
// Returns the group name based on depth
|
|
auto getGroupName = [&](int depth) {
|
|
QString gName;
|
|
for (int i = 0; i < depth + 1; ++i) {
|
|
gName.append((i == 0 ? "" : "/") + groups[i]);
|
|
}
|
|
return gName;
|
|
};
|
|
|
|
// Create new group(s) always when the path is not found
|
|
for (int i = 0; i < groups.length(); ++i) {
|
|
QString gName = getGroupName(i);
|
|
auto tempGroup = rootGroup->findGroupByPath(gName);
|
|
if (!tempGroup) {
|
|
Group* newGroup = new Group();
|
|
newGroup->setName(groups[i]);
|
|
newGroup->setUuid(QUuid::createUuid());
|
|
newGroup->setParent(previousGroup);
|
|
name = newGroup->name();
|
|
uuid = Tools::uuidToHex(newGroup->uuid());
|
|
previousGroup = newGroup;
|
|
continue;
|
|
}
|
|
|
|
previousGroup = tempGroup;
|
|
}
|
|
|
|
QJsonObject result;
|
|
result["name"] = name;
|
|
result["uuid"] = uuid;
|
|
return result;
|
|
}
|
|
|
|
QString BrowserService::getCurrentTotp(const QString& uuid)
|
|
{
|
|
QList<QSharedPointer<Database>> databases;
|
|
if (browserSettings()->searchInAllDatabases()) {
|
|
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
|
|
auto db = dbWidget->database();
|
|
if (db) {
|
|
databases << db;
|
|
}
|
|
}
|
|
} else {
|
|
databases << getDatabase();
|
|
}
|
|
|
|
auto entryUuid = Tools::hexToUuid(uuid);
|
|
for (const auto& db : databases) {
|
|
auto entry = db->rootGroup()->findEntryByUuid(entryUuid, true);
|
|
if (entry) {
|
|
return entry->totp();
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString BrowserService::storeKey(const QString& key)
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
bool contains;
|
|
auto dialogResult = MessageBox::Cancel;
|
|
QString id;
|
|
|
|
do {
|
|
QInputDialog keyDialog;
|
|
connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject()));
|
|
keyDialog.setWindowTitle(tr("KeePassXC: New key association request"));
|
|
keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n"
|
|
"Give the connection a unique name or ID, for example:\nchrome-laptop.")
|
|
.arg(db->metadata()->name().toHtmlEscaped()));
|
|
keyDialog.setOkButtonText(tr("Save and allow access"));
|
|
keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint);
|
|
raiseWindow();
|
|
keyDialog.show();
|
|
keyDialog.activateWindow();
|
|
keyDialog.raise();
|
|
auto ok = keyDialog.exec();
|
|
|
|
id = keyDialog.textValue();
|
|
|
|
if (ok != QDialog::Accepted || id.isEmpty() || !isDatabaseOpened()) {
|
|
hideWindow();
|
|
return {};
|
|
}
|
|
|
|
contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id);
|
|
if (contains) {
|
|
dialogResult = MessageBox::warning(nullptr,
|
|
tr("KeePassXC: Overwrite existing key?"),
|
|
tr("A shared encryption key with the name \"%1\" "
|
|
"already exists.\nDo you want to overwrite it?")
|
|
.arg(id),
|
|
MessageBox::Overwrite | MessageBox::Cancel,
|
|
MessageBox::Cancel);
|
|
}
|
|
} while (contains && dialogResult == MessageBox::Cancel);
|
|
|
|
hideWindow();
|
|
db->metadata()->customData()->set(CustomData::BrowserKeyPrefix + id, key);
|
|
db->metadata()->customData()->set(QString("%1_%2").arg(CustomData::Created, id),
|
|
Clock::currentDateTime().toString(Qt::SystemLocaleShortDate));
|
|
return id;
|
|
}
|
|
|
|
QString BrowserService::getKey(const QString& id)
|
|
{
|
|
auto db = getDatabase();
|
|
if (!db) {
|
|
return {};
|
|
}
|
|
|
|
return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
|
|
}
|
|
|
|
QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
|
|
const QString& siteUrlStr,
|
|
const QString& formUrlStr,
|
|
const QString& realm,
|
|
const StringPairList& keyList,
|
|
const bool httpAuth)
|
|
{
|
|
Q_UNUSED(dbid);
|
|
const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess();
|
|
const bool ignoreHttpAuth = browserSettings()->httpAuthPermission();
|
|
const QString siteHost = QUrl(siteUrlStr).host();
|
|
const QString formHost = QUrl(formUrlStr).host();
|
|
|
|
// Check entries for authorization
|
|
QList<Entry*> pwEntriesToConfirm;
|
|
QList<Entry*> pwEntries;
|
|
for (auto* entry : searchEntries(siteUrlStr, formUrlStr, keyList)) {
|
|
if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
|
|
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR) {
|
|
continue;
|
|
}
|
|
|
|
if (!httpAuth && entry->customData()->contains(BrowserService::OPTION_ONLY_HTTP_AUTH)
|
|
&& entry->customData()->value(BrowserService::OPTION_ONLY_HTTP_AUTH) == TRUE_STR) {
|
|
continue;
|
|
}
|
|
|
|
if (httpAuth && entry->customData()->contains(BrowserService::OPTION_NOT_HTTP_AUTH)
|
|
&& entry->customData()->value(BrowserService::OPTION_NOT_HTTP_AUTH) == TRUE_STR) {
|
|
continue;
|
|
}
|
|
|
|
// HTTP Basic Auth always needs a confirmation
|
|
if (!ignoreHttpAuth && httpAuth) {
|
|
pwEntriesToConfirm.append(entry);
|
|
continue;
|
|
}
|
|
|
|
switch (checkAccess(entry, siteHost, formHost, realm)) {
|
|
case Denied:
|
|
continue;
|
|
|
|
case Unknown:
|
|
if (alwaysAllowAccess) {
|
|
pwEntries.append(entry);
|
|
} else {
|
|
pwEntriesToConfirm.append(entry);
|
|
}
|
|
break;
|
|
|
|
case Allowed:
|
|
pwEntries.append(entry);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Confirm entries
|
|
QList<Entry*> selectedEntriesToConfirm =
|
|
confirmEntries(pwEntriesToConfirm, siteUrlStr, siteHost, formHost, realm, httpAuth);
|
|
if (!selectedEntriesToConfirm.isEmpty()) {
|
|
pwEntries.append(selectedEntriesToConfirm);
|
|
}
|
|
|
|
if (pwEntries.isEmpty()) {
|
|
return {};
|
|
}
|
|
|
|
// Ensure that database is not locked when the popup was visible
|
|
if (!isDatabaseOpened()) {
|
|
return {};
|
|
}
|
|
|
|
// Sort results
|
|
pwEntries = sortEntries(pwEntries, siteUrlStr, formUrlStr);
|
|
|
|
// Fill the list
|
|
QJsonArray result;
|
|
for (auto* entry : pwEntries) {
|
|
result.append(prepareEntry(entry));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void BrowserService::addEntry(const QString& dbid,
|
|
const QString& login,
|
|
const QString& password,
|
|
const QString& siteUrlStr,
|
|
const QString& formUrlStr,
|
|
const QString& realm,
|
|
const QString& group,
|
|
const QString& groupUuid,
|
|
const QSharedPointer<Database>& selectedDb)
|
|
{
|
|
// TODO: select database based on this key id
|
|
Q_UNUSED(dbid);
|
|
auto db = selectedDb ? selectedDb : selectedDatabase();
|
|
if (!db) {
|
|
return;
|
|
}
|
|
|
|
auto* entry = new Entry();
|
|
entry->setUuid(QUuid::createUuid());
|
|
entry->setTitle(QUrl(siteUrlStr).host());
|
|
entry->setUrl(siteUrlStr);
|
|
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
|
|
entry->setUsername(login);
|
|
entry->setPassword(password);
|
|
|
|
// Select a group for the entry
|
|
if (!group.isEmpty()) {
|
|
if (db->rootGroup()) {
|
|
auto selectedGroup = db->rootGroup()->findGroupByUuid(Tools::hexToUuid(groupUuid));
|
|
if (selectedGroup) {
|
|
entry->setGroup(selectedGroup);
|
|
} else {
|
|
entry->setGroup(getDefaultEntryGroup(db));
|
|
}
|
|
}
|
|
} else {
|
|
entry->setGroup(getDefaultEntryGroup(db));
|
|
}
|
|
|
|
const QString host = QUrl(siteUrlStr).host();
|
|
const QString submitHost = QUrl(formUrlStr).host();
|
|
BrowserEntryConfig config;
|
|
config.allow(host);
|
|
|
|
if (!submitHost.isEmpty()) {
|
|
config.allow(submitHost);
|
|
}
|
|
if (!realm.isEmpty()) {
|
|
config.setRealm(realm);
|
|
}
|
|
config.save(entry);
|
|
}
|
|
|
|
bool BrowserService::updateEntry(const QString& dbid,
|
|
const QString& uuid,
|
|
const QString& login,
|
|
const QString& password,
|
|
const QString& siteUrlStr,
|
|
const QString& formUrlStr)
|
|
{
|
|
// TODO: select database based on this key id
|
|
Q_UNUSED(dbid);
|
|
auto db = selectedDatabase();
|
|
if (!db) {
|
|
return false;
|
|
}
|
|
|
|
Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
|
|
if (!entry) {
|
|
// If entry is not found for update, add a new one to the selected database
|
|
addEntry(dbid, login, password, siteUrlStr, formUrlStr, "", "", "", db);
|
|
return true;
|
|
}
|
|
|
|
// Check if the entry password is a reference. If so, update the original entry instead
|
|
while (entry->attributes()->isReference(EntryAttributes::PasswordKey)) {
|
|
const QUuid referenceUuid = entry->attributes()->referenceUuid(EntryAttributes::PasswordKey);
|
|
if (!referenceUuid.isNull()) {
|
|
entry = db->rootGroup()->findEntryByUuid(referenceUuid);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
QString username = entry->username();
|
|
if (username.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
bool result = false;
|
|
if (username.compare(login, Qt::CaseSensitive) != 0
|
|
|| entry->password().compare(password, Qt::CaseSensitive) != 0) {
|
|
MessageBox::Button dialogResult = MessageBox::No;
|
|
if (!browserSettings()->alwaysAllowUpdate()) {
|
|
raiseWindow();
|
|
dialogResult = MessageBox::question(
|
|
nullptr,
|
|
tr("KeePassXC: Update Entry"),
|
|
tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrlStr).host(), username),
|
|
MessageBox::Save | MessageBox::Cancel,
|
|
MessageBox::Cancel,
|
|
MessageBox::Raise);
|
|
}
|
|
|
|
if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) {
|
|
entry->beginUpdate();
|
|
if (!entry->attributes()->isReference(EntryAttributes::UserNameKey)) {
|
|
entry->setUsername(login);
|
|
}
|
|
entry->setPassword(password);
|
|
entry->endUpdate();
|
|
result = true;
|
|
}
|
|
|
|
hideWindow();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QList<Entry*>
|
|
BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrlStr, const QString& formUrlStr)
|
|
{
|
|
QList<Entry*> entries;
|
|
auto* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return entries;
|
|
}
|
|
|
|
for (const auto& group : rootGroup->groupsRecursive(true)) {
|
|
if (group->isRecycled() || !group->resolveSearchingEnabled()) {
|
|
continue;
|
|
}
|
|
|
|
for (auto* entry : group->entries()) {
|
|
if (entry->isRecycled()) {
|
|
continue;
|
|
}
|
|
|
|
// Search for additional URL's starting with KP2A_URL
|
|
for (const auto& key : entry->attributes()->keys()) {
|
|
if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), siteUrlStr, formUrlStr)
|
|
&& !entries.contains(entry)) {
|
|
entries.append(entry);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!handleEntry(entry, siteUrlStr, formUrlStr)) {
|
|
continue;
|
|
}
|
|
|
|
// Additional URL check may have already inserted the entry to the list
|
|
if (!entries.contains(entry)) {
|
|
entries.append(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
QList<Entry*>
|
|
BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList)
|
|
{
|
|
// Check if database is connected with KeePassXC-Browser
|
|
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
|
|
for (const StringPair& keyPair : keyList) {
|
|
QString key = db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + keyPair.first);
|
|
if (!key.isEmpty() && keyPair.second == key) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Get the list of databases to search
|
|
QList<QSharedPointer<Database>> databases;
|
|
if (browserSettings()->searchInAllDatabases()) {
|
|
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
|
|
auto db = dbWidget->database();
|
|
if (db && databaseConnected(dbWidget->database())) {
|
|
databases << db;
|
|
}
|
|
}
|
|
} else {
|
|
const auto& db = getDatabase();
|
|
if (databaseConnected(db)) {
|
|
databases << db;
|
|
}
|
|
}
|
|
|
|
// Search entries matching the hostname
|
|
QString hostname = QUrl(siteUrlStr).host();
|
|
QList<Entry*> entries;
|
|
do {
|
|
for (const auto& db : databases) {
|
|
entries << searchEntries(db, siteUrlStr, formUrlStr);
|
|
}
|
|
} while (entries.isEmpty() && removeFirstDomain(hostname));
|
|
|
|
return entries;
|
|
}
|
|
|
|
void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db)
|
|
{
|
|
if (!db) {
|
|
return;
|
|
}
|
|
|
|
QList<Entry*> entries = db->rootGroup()->entriesRecursive();
|
|
QProgressDialog progress(tr("Converting attributes to custom data…"), tr("Abort"), 0, entries.count());
|
|
progress.setWindowModality(Qt::WindowModal);
|
|
|
|
int counter = 0;
|
|
int keyCounter = 0;
|
|
for (auto* entry : entries) {
|
|
if (progress.wasCanceled()) {
|
|
return;
|
|
}
|
|
|
|
if (moveSettingsToCustomData(entry, KEEPASSHTTP_NAME)) {
|
|
++counter;
|
|
}
|
|
|
|
if (moveSettingsToCustomData(entry, KEEPASSXCBROWSER_OLD_NAME)) {
|
|
++counter;
|
|
}
|
|
|
|
if (moveSettingsToCustomData(entry, KEEPASSXCBROWSER_NAME)) {
|
|
++counter;
|
|
}
|
|
|
|
if (entry->title() == KEEPASSHTTP_NAME || entry->title().contains(KEEPASSXCBROWSER_NAME, Qt::CaseInsensitive)) {
|
|
keyCounter += moveKeysToCustomData(entry, db);
|
|
db->recycleEntry(entry);
|
|
}
|
|
|
|
progress.setValue(progress.value() + 1);
|
|
}
|
|
progress.reset();
|
|
|
|
if (counter > 0) {
|
|
MessageBox::information(nullptr,
|
|
tr("KeePassXC: Converted KeePassHTTP attributes"),
|
|
tr("Successfully converted attributes from %1 entry(s).\n"
|
|
"Moved %2 keys to custom data.",
|
|
"")
|
|
.arg(counter)
|
|
.arg(keyCounter),
|
|
MessageBox::Ok);
|
|
} else if (counter == 0 && keyCounter > 0) {
|
|
MessageBox::information(nullptr,
|
|
tr("KeePassXC: Converted KeePassHTTP attributes"),
|
|
tr("Successfully moved %n keys to custom data.", "", keyCounter),
|
|
MessageBox::Ok);
|
|
} else {
|
|
MessageBox::information(nullptr,
|
|
tr("KeePassXC: No entry with KeePassHTTP attributes found!"),
|
|
tr("The active database does not contain an entry with KeePassHTTP attributes."),
|
|
MessageBox::Ok);
|
|
}
|
|
|
|
// Rename password groupName
|
|
Group* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return;
|
|
}
|
|
|
|
for (auto* g : rootGroup->groupsRecursive(true)) {
|
|
if (g->name() == KEEPASSHTTP_GROUP_NAME) {
|
|
g->setName(KEEPASSXCBROWSER_GROUP_NAME);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
QList<Entry*>
|
|
BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr)
|
|
{
|
|
// Build map of prioritized entries
|
|
QMultiMap<int, Entry*> priorities;
|
|
for (auto* entry : pwEntries) {
|
|
priorities.insert(sortPriority(getEntryURLs(entry), siteUrlStr, formUrlStr), entry);
|
|
}
|
|
|
|
auto keys = priorities.uniqueKeys();
|
|
std::sort(keys.begin(), keys.end(), [](int l, int r) { return l > r; });
|
|
|
|
QList<Entry*> results;
|
|
for (auto key : keys) {
|
|
results << priorities.values(key);
|
|
|
|
if (browserSettings()->bestMatchOnly() && !results.isEmpty()) {
|
|
// Early out once we find the highest batch of matches
|
|
break;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
|
|
const QString& siteUrlStr,
|
|
const QString& siteHost,
|
|
const QString& formUrlStr,
|
|
const QString& realm,
|
|
const bool httpAuth)
|
|
{
|
|
if (pwEntriesToConfirm.isEmpty() || m_dialogActive) {
|
|
return {};
|
|
}
|
|
|
|
m_dialogActive = true;
|
|
updateWindowState();
|
|
BrowserAccessControlDialog accessControlDialog;
|
|
|
|
connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject()));
|
|
|
|
connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) {
|
|
auto entry = pwEntriesToConfirm[item->row()];
|
|
BrowserEntryConfig config;
|
|
config.load(entry);
|
|
config.deny(siteHost);
|
|
if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
|
|
config.deny(formUrlStr);
|
|
}
|
|
if (!realm.isEmpty()) {
|
|
config.setRealm(realm);
|
|
}
|
|
config.save(entry);
|
|
});
|
|
|
|
accessControlDialog.setItems(pwEntriesToConfirm, siteUrlStr, httpAuth);
|
|
|
|
QList<Entry*> allowedEntries;
|
|
if (accessControlDialog.exec() == QDialog::Accepted) {
|
|
const auto selectedEntries = accessControlDialog.getSelectedEntries();
|
|
for (auto item : accessControlDialog.getSelectedEntries()) {
|
|
auto entry = pwEntriesToConfirm[item->row()];
|
|
if (accessControlDialog.remember()) {
|
|
BrowserEntryConfig config;
|
|
config.load(entry);
|
|
config.allow(siteHost);
|
|
if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
|
|
config.allow(formUrlStr);
|
|
}
|
|
if (!realm.isEmpty()) {
|
|
config.setRealm(realm);
|
|
}
|
|
config.save(entry);
|
|
}
|
|
allowedEntries.append(entry);
|
|
}
|
|
}
|
|
|
|
// Re-hide the application if it wasn't visible before
|
|
hideWindow();
|
|
|
|
m_dialogActive = false;
|
|
|
|
return allowedEntries;
|
|
}
|
|
|
|
QJsonObject BrowserService::prepareEntry(const Entry* entry)
|
|
{
|
|
QJsonObject res;
|
|
res["login"] = entry->resolveMultiplePlaceholders(entry->username());
|
|
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
|
|
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
|
|
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
|
|
res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
|
|
|
|
if (entry->hasTotp()) {
|
|
res["totp"] = entry->totp();
|
|
}
|
|
|
|
if (entry->isExpired()) {
|
|
res["expired"] = TRUE_STR;
|
|
}
|
|
|
|
if (entry->customData()->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) {
|
|
res["skipAutoSubmit"] = entry->customData()->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
|
|
}
|
|
|
|
if (browserSettings()->supportKphFields()) {
|
|
const EntryAttributes* attr = entry->attributes();
|
|
QJsonArray stringFields;
|
|
for (const auto& key : attr->keys()) {
|
|
if (key.startsWith("KPH: ")) {
|
|
QJsonObject sField;
|
|
sField[key] = entry->resolveMultiplePlaceholders(attr->value(key));
|
|
stringFields.append(sField);
|
|
}
|
|
}
|
|
res["stringFields"] = stringFields;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
BrowserService::Access
|
|
BrowserService::checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm)
|
|
{
|
|
if (entry->isExpired()) {
|
|
return browserSettings()->allowExpiredCredentials() ? Allowed : Denied;
|
|
}
|
|
|
|
BrowserEntryConfig config;
|
|
if (!config.load(entry)) {
|
|
return Unknown;
|
|
}
|
|
if ((config.isAllowed(siteHost)) && (formHost.isEmpty() || config.isAllowed(formHost))) {
|
|
return Allowed;
|
|
}
|
|
if ((config.isDenied(siteHost)) || (!formHost.isEmpty() && config.isDenied(formHost))) {
|
|
return Denied;
|
|
}
|
|
if (!realm.isEmpty() && config.realm() != realm) {
|
|
return Denied;
|
|
}
|
|
return Unknown;
|
|
}
|
|
|
|
Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb)
|
|
{
|
|
auto db = selectedDb ? selectedDb : getDatabase();
|
|
if (!db) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* rootGroup = db->rootGroup();
|
|
if (!rootGroup) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (auto* g : rootGroup->groupsRecursive(true)) {
|
|
if (g->name() == KEEPASSXCBROWSER_GROUP_NAME && !g->isRecycled()) {
|
|
return db->rootGroup()->findGroupByUuid(g->uuid());
|
|
}
|
|
}
|
|
|
|
auto* group = new Group();
|
|
group->setUuid(QUuid::createUuid());
|
|
group->setName(KEEPASSXCBROWSER_GROUP_NAME);
|
|
group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
|
|
group->setParent(rootGroup);
|
|
return group;
|
|
}
|
|
|
|
// Returns the maximum sort priority given a set of match urls and the
|
|
// extension provided site and form url.
|
|
int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr)
|
|
{
|
|
QList<int> priorityList;
|
|
// NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that
|
|
// by removing parts of the url that we don't match and direct matching others
|
|
const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo;
|
|
const auto siteUrl = QUrl(siteUrlStr).adjusted(stdOpts);
|
|
const auto formUrl = QUrl(formUrlStr).adjusted(stdOpts);
|
|
|
|
auto getPriority = [&](const QString& givenUrl) {
|
|
auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts);
|
|
|
|
// Default to https scheme if undefined
|
|
if (url.scheme().isEmpty() || !givenUrl.contains("://")) {
|
|
url.setScheme("https");
|
|
}
|
|
|
|
// Add the empty path to the URL if it's missing.
|
|
// URL's from the extension always have a path set, entry URL's can be without.
|
|
if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) {
|
|
url.setPath("/");
|
|
}
|
|
|
|
// Reject invalid urls and hosts, except 'localhost', and scheme mismatch
|
|
if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost")
|
|
|| url.scheme() != siteUrl.scheme()) {
|
|
return 0;
|
|
}
|
|
|
|
// Exact match with site url or form url
|
|
if (url.matches(siteUrl, QUrl::None) || url.matches(formUrl, QUrl::None)) {
|
|
return 100;
|
|
}
|
|
|
|
// Exact match without the query string
|
|
if (url.matches(siteUrl, QUrl::RemoveQuery) || url.matches(formUrl, QUrl::RemoveQuery)) {
|
|
return 90;
|
|
}
|
|
|
|
// Match without path (ie, FQDN match), form url prioritizes lower than site url
|
|
if (url.host() == siteUrl.host()) {
|
|
return 80;
|
|
}
|
|
if (url.host() == formUrl.host()) {
|
|
return 70;
|
|
}
|
|
|
|
// Site/form url ends with given url (subdomain mismatch)
|
|
if (siteUrl.host().endsWith(url.host())) {
|
|
return 60;
|
|
}
|
|
if (formUrl.host().endsWith(url.host())) {
|
|
return 50;
|
|
}
|
|
|
|
// No valid match found
|
|
return 0;
|
|
};
|
|
|
|
for (const auto& entryUrl : urls) {
|
|
priorityList << getPriority(entryUrl);
|
|
}
|
|
|
|
return *std::max_element(priorityList.begin(), priorityList.end());
|
|
}
|
|
|
|
bool BrowserService::schemeFound(const QString& url)
|
|
{
|
|
QUrl address(url);
|
|
return !address.scheme().isEmpty();
|
|
}
|
|
|
|
bool BrowserService::removeFirstDomain(QString& hostname)
|
|
{
|
|
int pos = hostname.indexOf(".");
|
|
if (pos < 0) {
|
|
return false;
|
|
}
|
|
|
|
// Don't remove the second-level domain if it's the only one
|
|
if (hostname.count(".") > 1) {
|
|
hostname = hostname.mid(pos + 1);
|
|
return !hostname.isEmpty();
|
|
}
|
|
|
|
// Nothing removed
|
|
return false;
|
|
}
|
|
|
|
/* Test if a search URL matches a custom entry. If the URL has the schema "keepassxc", some special checks will be made.
|
|
* Otherwise, this simply delegates to handleURL(). */
|
|
bool BrowserService::handleEntry(Entry* entry, const QString& url, const QString& submitUrl)
|
|
{
|
|
// Use this special scheme to find entries by UUID
|
|
if (url.startsWith("keepassxc://by-uuid/")) {
|
|
return url.endsWith("by-uuid/" + entry->uuidToHex());
|
|
} else if (url.startsWith("keepassxc://by-path/")) {
|
|
return url.endsWith("by-path/" + entry->path());
|
|
}
|
|
return handleURL(entry->url(), url, submitUrl);
|
|
}
|
|
|
|
bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr)
|
|
{
|
|
if (entryUrl.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
QUrl entryQUrl;
|
|
if (entryUrl.contains("://")) {
|
|
entryQUrl = entryUrl;
|
|
} else {
|
|
entryQUrl = QUrl::fromUserInput(entryUrl);
|
|
|
|
if (browserSettings()->matchUrlScheme()) {
|
|
entryQUrl.setScheme("https");
|
|
}
|
|
}
|
|
|
|
// Make a direct compare if a local file is used
|
|
if (siteUrlStr.startsWith("file://")) {
|
|
return entryUrl == formUrlStr;
|
|
}
|
|
|
|
// URL host validation fails
|
|
if (entryQUrl.host().isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// Match port, if used
|
|
QUrl siteQUrl(siteUrlStr);
|
|
if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
|
|
return false;
|
|
}
|
|
|
|
// Match scheme
|
|
if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty()
|
|
&& entryQUrl.scheme().compare(siteQUrl.scheme()) != 0) {
|
|
return false;
|
|
}
|
|
|
|
// Check for illegal characters
|
|
QRegularExpression re("[<>\\^`{|}]");
|
|
if (re.match(entryUrl).hasMatch()) {
|
|
return false;
|
|
}
|
|
|
|
// Match the base domain
|
|
if (baseDomain(siteQUrl.host()) != baseDomain(entryQUrl.host())) {
|
|
return false;
|
|
}
|
|
|
|
// Match the subdomains with the limited wildcard
|
|
if (siteQUrl.host().endsWith(entryQUrl.host())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Gets the base domain of URL.
|
|
*
|
|
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
|
|
*/
|
|
QString BrowserService::baseDomain(const QString& hostname) const
|
|
{
|
|
QUrl qurl = QUrl::fromUserInput(hostname);
|
|
QString host = qurl.host();
|
|
|
|
// If the hostname is an IP address, return it directly
|
|
QHostAddress hostAddress(hostname);
|
|
if (!hostAddress.isNull()) {
|
|
return hostname;
|
|
}
|
|
|
|
if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) {
|
|
return {};
|
|
}
|
|
|
|
// Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example
|
|
host.chop(qurl.topLevelDomain().length());
|
|
// Split the URL and select the last part, e.g. https://another.example -> example
|
|
QString baseDomain = host.split('.').last();
|
|
// Append the top level domain back to the URL, e.g. example -> example.co.uk
|
|
baseDomain.append(qurl.topLevelDomain());
|
|
return baseDomain;
|
|
}
|
|
|
|
QSharedPointer<Database> BrowserService::getDatabase()
|
|
{
|
|
if (m_currentDatabaseWidget) {
|
|
return m_currentDatabaseWidget->database();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QSharedPointer<Database> BrowserService::selectedDatabase()
|
|
{
|
|
QList<DatabaseWidget*> databaseWidgets;
|
|
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
|
|
// Add only open databases
|
|
if (!dbWidget->isLocked()) {
|
|
databaseWidgets << dbWidget;
|
|
}
|
|
}
|
|
|
|
BrowserEntrySaveDialog browserEntrySaveDialog;
|
|
int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget);
|
|
if (openDatabaseCount > 1) {
|
|
int res = browserEntrySaveDialog.exec();
|
|
if (res == QDialog::Accepted) {
|
|
const auto selectedDatabase = browserEntrySaveDialog.getSelected();
|
|
if (selectedDatabase.length() > 0) {
|
|
int index = selectedDatabase[0]->data(Qt::UserRole).toInt();
|
|
return databaseWidgets[index]->database();
|
|
}
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Return current database
|
|
return getDatabase();
|
|
}
|
|
|
|
bool BrowserService::moveSettingsToCustomData(Entry* entry, const QString& name)
|
|
{
|
|
if (entry->attributes()->contains(name)) {
|
|
QString attr = entry->attributes()->value(name);
|
|
entry->beginUpdate();
|
|
if (!attr.isEmpty()) {
|
|
entry->customData()->set(KEEPASSXCBROWSER_NAME, attr);
|
|
}
|
|
entry->attributes()->remove(name);
|
|
entry->endUpdate();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int BrowserService::moveKeysToCustomData(Entry* entry, QSharedPointer<Database> db)
|
|
{
|
|
int keyCounter = 0;
|
|
for (const auto& key : entry->attributes()->keys()) {
|
|
if (key.contains(CustomData::BrowserLegacyKeyPrefix)) {
|
|
QString publicKey = key;
|
|
publicKey.remove(CustomData::BrowserLegacyKeyPrefix);
|
|
|
|
// Add key to database custom data
|
|
if (db && !db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + publicKey)) {
|
|
db->metadata()->customData()->set(CustomData::BrowserKeyPrefix + publicKey,
|
|
entry->attributes()->value(key));
|
|
++keyCounter;
|
|
}
|
|
}
|
|
}
|
|
|
|
return keyCounter;
|
|
}
|
|
|
|
bool BrowserService::checkLegacySettings(QSharedPointer<Database> db)
|
|
{
|
|
if (!db || !browserSettings()->isEnabled() || browserSettings()->noMigrationPrompt()) {
|
|
return false;
|
|
}
|
|
|
|
bool legacySettingsFound = false;
|
|
QList<Entry*> entries = db->rootGroup()->entriesRecursive();
|
|
for (const auto& e : entries) {
|
|
if (e->isRecycled()) {
|
|
continue;
|
|
}
|
|
|
|
if ((e->attributes()->contains(KEEPASSHTTP_NAME) || e->attributes()->contains(KEEPASSXCBROWSER_NAME))
|
|
|| (e->title() == KEEPASSHTTP_NAME || e->title().contains(KEEPASSXCBROWSER_NAME, Qt::CaseInsensitive))) {
|
|
legacySettingsFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!legacySettingsFound) {
|
|
return false;
|
|
}
|
|
|
|
auto* checkbox = new QCheckBox(tr("Don't show this warning again"));
|
|
QObject::connect(checkbox, &QCheckBox::stateChanged, [&](int state) {
|
|
browserSettings()->setNoMigrationPrompt(static_cast<Qt::CheckState>(state) == Qt::CheckState::Checked);
|
|
});
|
|
|
|
auto dialogResult =
|
|
MessageBox::warning(nullptr,
|
|
tr("KeePassXC: Legacy browser integration settings detected"),
|
|
tr("Your KeePassXC-Browser settings need to be moved into the database settings.\n"
|
|
"This is necessary to maintain your current browser connections.\n"
|
|
"Would you like to migrate your existing settings now?"),
|
|
MessageBox::Yes | MessageBox::No,
|
|
MessageBox::NoButton,
|
|
MessageBox::Raise,
|
|
checkbox);
|
|
|
|
return dialogResult == MessageBox::Yes;
|
|
}
|
|
|
|
QStringList BrowserService::getEntryURLs(const Entry* entry)
|
|
{
|
|
QStringList urlList;
|
|
urlList << entry->url();
|
|
|
|
// Handle additional URL's
|
|
for (const auto& key : entry->attributes()->keys()) {
|
|
if (key.startsWith(ADDITIONAL_URL)) {
|
|
urlList << entry->attributes()->value(key);
|
|
}
|
|
}
|
|
|
|
return urlList;
|
|
}
|
|
|
|
void BrowserService::hideWindow() const
|
|
{
|
|
if (m_prevWindowState == WindowState::Minimized) {
|
|
getMainWindow()->showMinimized();
|
|
} else {
|
|
#ifdef Q_OS_MACOS
|
|
if (m_prevWindowState == WindowState::Hidden) {
|
|
macUtils()->hideOwnWindow();
|
|
} else {
|
|
macUtils()->raiseLastActiveWindow();
|
|
}
|
|
#else
|
|
if (m_prevWindowState == WindowState::Hidden) {
|
|
getMainWindow()->hideWindow();
|
|
} else {
|
|
getMainWindow()->lower();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void BrowserService::raiseWindow(const bool force)
|
|
{
|
|
m_prevWindowState = WindowState::Normal;
|
|
if (getMainWindow()->isMinimized()) {
|
|
m_prevWindowState = WindowState::Minimized;
|
|
}
|
|
#ifdef Q_OS_MACOS
|
|
Q_UNUSED(force)
|
|
|
|
if (macUtils()->isHidden()) {
|
|
m_prevWindowState = WindowState::Hidden;
|
|
}
|
|
macUtils()->raiseOwnWindow();
|
|
Tools::wait(500);
|
|
#else
|
|
if (getMainWindow()->isHidden()) {
|
|
m_prevWindowState = WindowState::Hidden;
|
|
}
|
|
|
|
if (force) {
|
|
getMainWindow()->bringToFront();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void BrowserService::updateWindowState()
|
|
{
|
|
m_prevWindowState = WindowState::Normal;
|
|
if (getMainWindow()->isMinimized()) {
|
|
m_prevWindowState = WindowState::Minimized;
|
|
}
|
|
#ifdef Q_OS_MACOS
|
|
if (macUtils()->isHidden()) {
|
|
m_prevWindowState = WindowState::Hidden;
|
|
}
|
|
#else
|
|
if (getMainWindow()->isHidden()) {
|
|
m_prevWindowState = WindowState::Hidden;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void BrowserService::databaseLocked(DatabaseWidget* dbWidget)
|
|
{
|
|
if (dbWidget) {
|
|
QJsonObject msg;
|
|
msg["action"] = QString("database-locked");
|
|
m_browserHost->sendClientMessage(msg);
|
|
}
|
|
}
|
|
|
|
void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
|
|
{
|
|
if (dbWidget) {
|
|
if (m_bringToFrontRequested) {
|
|
m_bringToFrontRequested = false;
|
|
hideWindow();
|
|
}
|
|
|
|
QJsonObject msg;
|
|
msg["action"] = QString("database-unlocked");
|
|
m_browserHost->sendClientMessage(msg);
|
|
|
|
auto db = dbWidget->database();
|
|
if (checkLegacySettings(db)) {
|
|
convertAttributesToCustomData(db);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BrowserService::activeDatabaseChanged(DatabaseWidget* dbWidget)
|
|
{
|
|
// Only emit these signals when we are not searching in all databases
|
|
if (dbWidget && !browserSettings()->searchInAllDatabases()) {
|
|
if (dbWidget->isLocked()) {
|
|
databaseLocked(dbWidget);
|
|
} else {
|
|
databaseUnlocked(dbWidget);
|
|
}
|
|
}
|
|
|
|
m_currentDatabaseWidget = dbWidget;
|
|
}
|
|
|
|
void BrowserService::processClientMessage(const QJsonObject& message)
|
|
{
|
|
auto clientID = message["clientID"].toString();
|
|
if (clientID.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Create a new client action if we haven't seen this id yet
|
|
if (!m_browserClients.contains(clientID)) {
|
|
m_browserClients.insert(clientID, QSharedPointer<BrowserAction>::create());
|
|
}
|
|
|
|
auto& action = m_browserClients.value(clientID);
|
|
auto response = action->processClientMessage(message);
|
|
m_browserHost->sendClientMessage(response);
|
|
}
|