2
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
4
* Copyright (C) 2013 Francois Ferrand
6
* This program is free software: you can redistribute it and/or modify
7
* it under the terms of the GNU General Public License as published by
8
* the Free Software Foundation, either version 3 of the License, or
9
* (at your option) any later version.
11
* This program is distributed in the hope that it will be useful,
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
* GNU General Public License for more details.
16
* You should have received a copy of the GNU General Public License
17
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#include "BrowserService.h"
21
#include "BrowserAction.h"
22
#include "BrowserEntryConfig.h"
23
#include "BrowserEntrySaveDialog.h"
24
#include "BrowserHost.h"
25
#include "BrowserMessageBuilder.h"
26
#include "BrowserSettings.h"
27
#include "core/Tools.h"
28
#include "core/UrlTools.h"
29
#include "gui/MainWindow.h"
30
#include "gui/MessageBox.h"
31
#include "gui/osutils/OSUtils.h"
32
#ifdef WITH_XC_BROWSER_PASSKEYS
33
#include "BrowserPasskeys.h"
34
#include "BrowserPasskeysClient.h"
35
#include "BrowserPasskeysConfirmationDialog.h"
36
#include "PasskeyUtils.h"
37
#include "gui/passkeys/PasskeyImporter.h"
40
#include "gui/osutils/macutils/MacUtils.h"
44
#include <QCryptographicHash>
45
#include <QHostAddress>
46
#include <QInputDialog>
50
#include <QLocalSocket>
51
#include <QProgressDialog>
54
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
55
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
56
static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
57
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
58
#ifdef WITH_XC_BROWSER_PASSKEYS
59
static int KEEPASSXCBROWSER_PASSKEY_ICON = 13;
61
// These are for the settings and password conversion
62
static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
63
static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
64
// Extra entry related options saved in custom data
65
const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit");
66
const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
67
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
68
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
69
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
70
const QString BrowserService::OPTION_RESTRICT_KEY = QStringLiteral("BrowserRestrictKey");
72
Q_GLOBAL_STATIC(BrowserService, s_browserService);
74
BrowserService::BrowserService()
76
, m_browserHost(new BrowserHost)
77
, m_dialogActive(false)
78
, m_bringToFrontRequested(false)
79
, m_prevWindowState(WindowState::Normal)
80
, m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
82
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage);
83
connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked);
84
connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
85
connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
87
setEnabled(browserSettings()->isEnabled());
90
BrowserService* BrowserService::instance()
92
return s_browserService;
95
void BrowserService::setEnabled(bool enabled)
98
// Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts
99
if (browserSettings()->updateBinaryPath()) {
100
browserSettings()->updateBinaryPaths();
103
m_browserHost->start();
105
m_browserHost->stop();
109
bool BrowserService::isDatabaseOpened() const
111
if (m_currentDatabaseWidget) {
112
return !m_currentDatabaseWidget->isLocked();
117
bool BrowserService::openDatabase(bool triggerUnlock)
119
if (!browserSettings()->unlockDatabase()) {
123
if (m_currentDatabaseWidget && !m_currentDatabaseWidget->isLocked()) {
127
if (triggerUnlock && !m_bringToFrontRequested) {
128
m_bringToFrontRequested = true;
130
emit requestUnlock();
136
void BrowserService::lockDatabase()
138
if (m_currentDatabaseWidget) {
139
m_currentDatabaseWidget->lock();
143
QString BrowserService::getDatabaseHash(bool legacy)
146
return QCryptographicHash::hash(
147
(browserService()->getDatabaseRootUuid() + browserService()->getDatabaseRecycleBinUuid()).toUtf8(),
148
QCryptographicHash::Sha256)
151
return QCryptographicHash::hash(getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex();
154
QString BrowserService::getDatabaseRootUuid()
156
auto db = getDatabase();
161
Group* rootGroup = db->rootGroup();
166
return rootGroup->uuidToHex();
169
QString BrowserService::getDatabaseRecycleBinUuid()
171
auto db = getDatabase();
176
Group* recycleBin = db->metadata()->recycleBin();
180
return recycleBin->uuidToHex();
183
QJsonArray BrowserService::getChildrenFromGroup(Group* group)
185
QJsonArray groupList;
191
for (const auto& c : group->children()) {
192
if (c == group->database()->metadata()->recycleBin()) {
196
QJsonObject jsonGroup;
197
jsonGroup["name"] = c->name();
198
jsonGroup["uuid"] = Tools::uuidToHex(c->uuid());
199
jsonGroup["children"] = getChildrenFromGroup(c);
200
groupList.push_back(jsonGroup);
205
QJsonObject BrowserService::getDatabaseGroups()
207
auto db = getDatabase();
212
Group* rootGroup = db->rootGroup();
218
root["name"] = rootGroup->name();
219
root["uuid"] = Tools::uuidToHex(rootGroup->uuid());
220
root["children"] = getChildrenFromGroup(rootGroup);
223
groups.push_back(root);
226
result["groups"] = groups;
231
QJsonArray BrowserService::getDatabaseEntries()
233
auto db = getDatabase();
238
Group* rootGroup = db->rootGroup();
244
for (const auto& group : rootGroup->groupsRecursive(true)) {
245
if (group == db->metadata()->recycleBin()) {
249
for (const auto& entry : group->entries()) {
251
jentry["title"] = entry->resolveMultiplePlaceholders(entry->title());
252
jentry["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
253
jentry["url"] = entry->resolveMultiplePlaceholders(entry->url());
254
entries.push_back(jentry);
260
QJsonObject BrowserService::createNewGroup(const QString& groupName)
262
auto db = getDatabase();
267
Group* rootGroup = db->rootGroup();
272
auto group = rootGroup->findGroupByPath(groupName);
274
// Group already exists
277
result["name"] = group->name();
278
result["uuid"] = Tools::uuidToHex(group->uuid());
282
auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
283
tr("KeePassXC - Create a new group"),
284
tr("A request for creating a new group \"%1\" has been received.\n"
285
"Do you want to create this group?\n")
287
MessageBox::Yes | MessageBox::No);
289
if (dialogResult != MessageBox::Yes) {
294
Group* previousGroup = rootGroup;
295
auto groups = groupName.split("/");
297
// Returns the group name based on depth
298
auto getGroupName = [&](int depth) {
300
for (int i = 0; i < depth + 1; ++i) {
301
gName.append((i == 0 ? "" : "/") + groups[i]);
306
// Create new group(s) always when the path is not found
307
for (int i = 0; i < groups.length(); ++i) {
308
QString gName = getGroupName(i);
309
auto tempGroup = rootGroup->findGroupByPath(gName);
311
Group* newGroup = new Group();
312
newGroup->setName(groups[i]);
313
newGroup->setUuid(QUuid::createUuid());
314
newGroup->setParent(previousGroup);
315
name = newGroup->name();
316
uuid = Tools::uuidToHex(newGroup->uuid());
317
previousGroup = newGroup;
321
previousGroup = tempGroup;
325
result["name"] = name;
326
result["uuid"] = uuid;
330
QString BrowserService::getCurrentTotp(const QString& uuid)
332
QList<QSharedPointer<Database>> databases;
333
if (browserSettings()->searchInAllDatabases()) {
334
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
335
auto db = dbWidget->database();
341
databases << getDatabase();
344
auto entryUuid = Tools::hexToUuid(uuid);
345
for (const auto& db : databases) {
346
auto entry = db->rootGroup()->findEntryByUuid(entryUuid, true);
348
return entry->totp();
356
BrowserService::findEntries(const EntryParameters& entryParameters, const StringPairList& keyList, bool* entriesFound)
359
*entriesFound = false;
362
const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess();
363
const bool ignoreHttpAuth = browserSettings()->httpAuthPermission();
364
const QString siteHost = QUrl(entryParameters.siteUrl).host();
365
const QString formHost = QUrl(entryParameters.formUrl).host();
367
// Check entries for authorization
368
QList<Entry*> entriesToConfirm;
369
QList<Entry*> allowedEntries;
370
for (auto* entry : searchEntries(entryParameters.siteUrl, entryParameters.formUrl, keyList)) {
371
auto entryCustomData = entry->customData();
373
if (!entryParameters.httpAuth
374
&& ((entryCustomData->contains(BrowserService::OPTION_ONLY_HTTP_AUTH)
375
&& entryCustomData->value(BrowserService::OPTION_ONLY_HTTP_AUTH) == TRUE_STR)
376
|| entry->group()->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Enable)) {
380
if (entryParameters.httpAuth
381
&& ((entryCustomData->contains(BrowserService::OPTION_NOT_HTTP_AUTH)
382
&& entryCustomData->value(BrowserService::OPTION_NOT_HTTP_AUTH) == TRUE_STR)
383
|| entry->group()->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Enable)) {
387
// HTTP Basic Auth always needs a confirmation
388
if (!ignoreHttpAuth && entryParameters.httpAuth) {
389
entriesToConfirm.append(entry);
393
switch (checkAccess(entry, siteHost, formHost, entryParameters.realm)) {
398
if (alwaysAllowAccess) {
399
allowedEntries.append(entry);
401
entriesToConfirm.append(entry);
406
allowedEntries.append(entry);
411
if (entriesToConfirm.isEmpty() && allowedEntries.isEmpty()) {
416
auto selectedEntriesToConfirm =
417
confirmEntries(entriesToConfirm, entryParameters, siteHost, formHost, entryParameters.httpAuth);
418
if (!selectedEntriesToConfirm.isEmpty()) {
419
allowedEntries.append(selectedEntriesToConfirm);
422
// Ensure that database is not locked when the popup was visible
423
if (!isDatabaseOpened()) {
428
allowedEntries = sortEntries(allowedEntries, entryParameters.siteUrl, entryParameters.formUrl);
432
for (auto* entry : allowedEntries) {
433
entries.append(prepareEntry(entry));
436
if (entriesFound != nullptr) {
437
*entriesFound = true;
443
QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
444
const EntryParameters& entryParameters,
445
const QString& siteHost,
446
const QString& formUrl,
449
if (entriesToConfirm.isEmpty() || m_dialogActive) {
453
m_dialogActive = true;
455
BrowserAccessControlDialog accessControlDialog(m_currentDatabaseWidget);
457
connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject()));
459
connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) {
460
auto entry = entriesToConfirm[item->row()];
461
denyEntry(entry, siteHost, formUrl, entryParameters.realm);
464
accessControlDialog.setEntries(entriesToConfirm, entryParameters.siteUrl, httpAuth);
466
QList<Entry*> allowedEntries;
467
auto ret = accessControlDialog.exec();
468
auto remember = accessControlDialog.remember();
471
if (ret == QDialog::Rejected && remember) {
472
for (auto& entry : entriesToConfirm) {
473
denyEntry(entry, siteHost, formUrl, entryParameters.realm);
477
// Some/all are accepted
478
if (ret == QDialog::Accepted) {
479
auto selectedEntries = accessControlDialog.getEntries(SelectionType::Selected);
480
for (auto& item : selectedEntries) {
481
auto entry = entriesToConfirm[item->row()];
482
allowedEntries.append(entry);
485
allowEntry(entry, siteHost, formUrl, entryParameters.realm);
489
// Remembered non-selected entries must be denied
491
auto nonSelectedEntries = accessControlDialog.getEntries(SelectionType::NonSelected);
492
for (auto& item : nonSelectedEntries) {
493
auto entry = entriesToConfirm[item->row()];
494
denyEntry(entry, siteHost, formUrl, entryParameters.realm);
499
// Handle disabled entries (returned Accept/Reject status does not matter)
500
auto disabledEntries = accessControlDialog.getEntries(SelectionType::Disabled);
501
for (auto& item : disabledEntries) {
502
auto entry = entriesToConfirm[item->row()];
503
denyEntry(entry, siteHost, formUrl, entryParameters.realm);
506
// Re-hide the application if it wasn't visible before
508
m_dialogActive = false;
510
return allowedEntries;
513
void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
515
if (!m_passwordGenerator) {
516
m_passwordGenerator = PasswordGeneratorWidget::popupGenerator();
518
connect(m_passwordGenerator.data(), &PasswordGeneratorWidget::closed, m_passwordGenerator.data(), [=] {
519
if (!m_passwordGenerator->isPasswordGenerated()) {
520
auto errorMessage = browserMessageBuilder()->getErrorReply("generate-password",
521
ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
522
m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage);
525
QTimer::singleShot(50, this, [&] { hideWindow(); });
528
connect(m_passwordGenerator.data(),
529
&PasswordGeneratorWidget::appliedPassword,
530
m_passwordGenerator.data(),
531
[=](const QString& password) {
532
const Parameters params{{"password", password}};
533
m_browserHost->sendClientMessage(keyPairMessage.socket,
534
browserMessageBuilder()->buildResponse("generate-password",
535
keyPairMessage.nonce,
537
keyPairMessage.publicKey,
538
keyPairMessage.secretKey));
543
m_passwordGenerator->show();
544
m_passwordGenerator->raise();
545
m_passwordGenerator->activateWindow();
548
bool BrowserService::isPasswordGeneratorRequested() const
550
return m_passwordGenerator && m_passwordGenerator->isVisible();
553
QString BrowserService::storeKey(const QString& key)
555
auto db = getDatabase();
561
auto dialogResult = MessageBox::Cancel;
565
QInputDialog keyDialog(m_currentDatabaseWidget);
566
connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject()));
567
keyDialog.setWindowTitle(tr("KeePassXC - New key association request"));
568
keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n"
569
"Give the connection a unique name or ID, for example:\nchrome-laptop.")
570
.arg(db->metadata()->name().toHtmlEscaped()));
571
keyDialog.setOkButtonText(tr("Save and allow access"));
572
keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint);
575
keyDialog.activateWindow();
577
auto ok = keyDialog.exec();
579
id = keyDialog.textValue();
581
if (ok != QDialog::Accepted || id.isEmpty() || !isDatabaseOpened()) {
586
contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id);
588
dialogResult = MessageBox::warning(m_currentDatabaseWidget,
589
tr("KeePassXC - Overwrite existing key?"),
590
tr("A shared encryption key with the name \"%1\" "
591
"already exists.\nDo you want to overwrite it?")
593
MessageBox::Overwrite | MessageBox::Cancel,
596
} while (contains && dialogResult == MessageBox::Cancel);
599
db->metadata()->customData()->set(CustomData::BrowserKeyPrefix + id, key);
600
db->metadata()->customData()->set(QString("%1_%2").arg(CustomData::Created, id),
601
Clock::currentDateTime().toString(Qt::SystemLocaleShortDate));
605
QString BrowserService::getKey(const QString& id)
607
auto db = getDatabase();
612
return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
615
#ifdef WITH_XC_BROWSER_PASSKEYS
616
// Passkey registration
617
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
618
const QString& origin,
619
const StringPairList& keyList)
621
auto db = selectedDatabase();
623
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
626
QJsonObject credentialCreationOptions;
627
const auto pkOptionsResult =
628
browserPasskeysClient()->getCredentialCreationOptions(publicKeyOptions, origin, &credentialCreationOptions);
629
if (pkOptionsResult > 0 || credentialCreationOptions.isEmpty()) {
630
return getPasskeyError(pkOptionsResult);
633
const auto excludeCredentials = credentialCreationOptions["excludeCredentials"].toArray();
634
const auto rpId = credentialCreationOptions["rp"].toObject()["id"].toString();
635
const auto timeout = publicKeyOptions["timeout"].toInt();
636
const auto username = credentialCreationOptions["user"].toObject()["name"].toString();
637
const auto user = credentialCreationOptions["user"].toObject();
638
const auto userId = user["id"].toString();
640
// Parse excludeCredentialDescriptorList
641
if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, rpId, keyList)) {
642
return getPasskeyError(ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED);
645
const auto existingEntries = getPasskeyEntriesWithUserHandle(rpId, userId, keyList);
648
BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
649
confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
651
auto dialogResult = confirmDialog.exec();
652
if (dialogResult == QDialog::Accepted) {
653
const auto publicKeyCredentials =
654
browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions);
655
if (publicKeyCredentials.credentialId.isEmpty() || publicKeyCredentials.key.isEmpty()
656
|| publicKeyCredentials.response.isEmpty()) {
657
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
660
const auto rpName = publicKeyOptions["rp"]["name"].toString();
661
if (confirmDialog.isPasskeyUpdated()) {
662
// If no entry is selected, show the import dialog for manual entry selection
663
auto selectedEntry = confirmDialog.getSelectedEntry();
664
if (!selectedEntry) {
665
PasskeyImporter passkeyImporter(m_currentDatabaseWidget);
666
const auto result = passkeyImporter.showImportDialog(db,
671
publicKeyCredentials.credentialId,
673
publicKeyCredentials.key,
674
tr("KeePassXC - Passkey credentials"),
675
tr("Register a new passkey to this entry:"),
678
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
681
addPasskeyToEntry(selectedEntry,
685
publicKeyCredentials.credentialId,
687
publicKeyCredentials.key);
690
addPasskeyToGroup(db,
696
publicKeyCredentials.credentialId,
698
publicKeyCredentials.key);
702
return publicKeyCredentials.response;
706
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
709
// Passkey authentication
710
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
711
const QString& origin,
712
const StringPairList& keyList)
714
auto db = getDatabase();
716
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
719
QJsonObject assertionOptions;
720
const auto assertionResult =
721
browserPasskeysClient()->getAssertionOptions(publicKeyOptions, origin, &assertionOptions);
722
if (assertionResult > 0 || assertionOptions.isEmpty()) {
723
return getPasskeyError(assertionResult);
726
// Get allowed entries from RP ID
727
const auto rpId = assertionOptions["rpId"].toString();
728
const auto entries = getPasskeyAllowedEntries(assertionOptions, rpId, keyList);
729
if (entries.isEmpty()) {
730
return getPasskeyError(ERROR_KEEPASS_NO_LOGINS_FOUND);
733
const auto timeout = publicKeyOptions["timeout"].toInt();
736
BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
737
confirmDialog.authenticateCredential(entries, rpId, timeout);
738
auto dialogResult = confirmDialog.exec();
739
if (dialogResult == QDialog::Accepted) {
741
const auto selectedEntry = confirmDialog.getSelectedEntry();
742
if (!selectedEntry) {
743
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
746
const auto privateKeyPem = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
747
const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
748
const auto userHandle = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
750
auto publicKeyCredential =
751
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
752
if (publicKeyCredential.isEmpty()) {
753
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
756
return publicKeyCredential;
760
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
763
void BrowserService::addPasskeyToGroup(const QSharedPointer<Database>& db,
767
const QString& rpName,
768
const QString& username,
769
const QString& credentialId,
770
const QString& userHandle,
771
const QString& privateKey)
773
// If no group provided, use the default browser group of the selected database
778
group = getDefaultEntryGroup(db);
781
auto* entry = new Entry();
782
entry->setUuid(QUuid::createUuid());
783
entry->setGroup(group);
784
entry->setTitle(tr("%1 (Passkey)").arg(rpName));
785
entry->setUsername(username);
787
entry->setIcon(KEEPASSXCBROWSER_PASSKEY_ICON);
789
addPasskeyToEntry(entry, rpId, rpName, username, credentialId, userHandle, privateKey);
791
// Remove blank entry history
792
entry->removeHistoryItems(entry->historyItems());
795
void BrowserService::addPasskeyToEntry(Entry* entry,
797
const QString& rpName,
798
const QString& username,
799
const QString& credentialId,
800
const QString& userHandle,
801
const QString& privateKey)
803
// Reserved for future use
811
// Ask confirmation if entry already contains a Passkey
812
if (entry->hasPasskey()) {
813
if (MessageBox::question(m_currentDatabaseWidget,
814
tr("KeePassXC - Update passkey"),
815
tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?")
816
.arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)),
817
MessageBox::Overwrite | MessageBox::Cancel,
819
!= MessageBox::Overwrite) {
824
entry->beginUpdate();
826
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
827
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
828
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
829
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
830
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
831
entry->addTag(tr("Passkey"));
837
void BrowserService::addEntry(const EntryParameters& entryParameters,
838
const QString& group,
839
const QString& groupUuid,
840
const bool downloadFavicon,
841
const QSharedPointer<Database>& selectedDb)
843
// TODO: select database based on this key id
844
auto db = selectedDb ? selectedDb : selectedDatabase();
849
auto* entry = new Entry();
850
entry->setUuid(QUuid::createUuid());
851
entry->setTitle(entryParameters.title.isEmpty() ? QUrl(entryParameters.siteUrl).host() : entryParameters.title);
852
entry->setUrl(entryParameters.siteUrl);
853
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
854
entry->setUsername(entryParameters.login);
855
entry->setPassword(entryParameters.password);
857
// Select a group for the entry
858
if (!group.isEmpty()) {
859
if (db->rootGroup()) {
860
auto selectedGroup = db->rootGroup()->findGroupByUuid(Tools::hexToUuid(groupUuid));
862
entry->setGroup(selectedGroup);
864
entry->setGroup(getDefaultEntryGroup(db));
868
entry->setGroup(getDefaultEntryGroup(db));
871
const QString host = QUrl(entryParameters.siteUrl).host();
872
const QString submitHost = QUrl(entryParameters.formUrl).host();
873
BrowserEntryConfig config;
876
if (!submitHost.isEmpty()) {
877
config.allow(submitHost);
879
if (!entryParameters.realm.isEmpty()) {
880
config.setRealm(entryParameters.realm);
884
if (downloadFavicon && m_currentDatabaseWidget) {
885
m_currentDatabaseWidget->downloadFaviconInBackground(entry);
889
bool BrowserService::updateEntry(const EntryParameters& entryParameters, const QString& uuid)
891
// TODO: select database based on this key id
892
auto db = selectedDatabase();
897
auto entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
899
// If entry is not found for update, add a new one to the selected database
900
addEntry(entryParameters, "", "", false, db);
904
// Check if the entry password is a reference. If so, update the original entry instead
905
while (entry->attributes()->isReference(EntryAttributes::PasswordKey)) {
906
const QUuid referenceUuid = entry->attributes()->referenceUuid(EntryAttributes::PasswordKey);
907
if (!referenceUuid.isNull()) {
908
entry = db->rootGroup()->findEntryByUuid(referenceUuid);
915
auto username = entry->username();
916
if (username.isEmpty()) {
921
if (username.compare(entryParameters.login, Qt::CaseSensitive) != 0
922
|| entry->password().compare(entryParameters.password, Qt::CaseSensitive) != 0) {
923
MessageBox::Button dialogResult = MessageBox::No;
924
if (!browserSettings()->alwaysAllowUpdate()) {
926
dialogResult = MessageBox::question(m_currentDatabaseWidget,
927
tr("KeePassXC - Update Entry"),
928
tr("Do you want to update the information in %1 - %2?")
929
.arg(QUrl(entryParameters.siteUrl).host(), username),
930
MessageBox::Save | MessageBox::Cancel,
934
if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) {
935
entry->beginUpdate();
936
if (!entry->attributes()->isReference(EntryAttributes::UserNameKey)) {
937
entry->setUsername(entryParameters.login);
939
entry->setPassword(entryParameters.password);
950
bool BrowserService::deleteEntry(const QString& uuid)
952
auto db = selectedDatabase();
957
auto* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
962
auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
963
tr("KeePassXC - Delete entry"),
964
tr("A request for deleting entry \"%1\" has been received.\n"
965
"Do you want to delete the entry?\n")
966
.arg(entry->title()),
967
MessageBox::Yes | MessageBox::No);
968
if (dialogResult != MessageBox::Yes) {
972
db->recycleEntry(entry);
976
QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
977
const QString& siteUrl,
978
const QString& formUrl,
979
const QStringList& keys,
982
QList<Entry*> entries;
983
auto* rootGroup = db->rootGroup();
988
for (const auto& group : rootGroup->groupsRecursive(true)) {
989
if (group->isRecycled()
990
|| group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable) {
994
// If a key restriction is specified and not contained in the keys list then skip this group.
995
auto restrictKey = group->resolveCustomDataString(BrowserService::OPTION_RESTRICT_KEY);
996
if (!restrictKey.isEmpty() && !keys.contains(restrictKey)) {
1000
const auto omitWwwSubdomain =
1001
group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW) == Group::Enable;
1003
for (auto* entry : group->entries()) {
1004
if (entry->isRecycled()
1005
|| (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
1006
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
1010
if (!passkey && !shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
1014
#ifdef WITH_XC_BROWSER_PASSKEYS
1015
// With Passkeys, check for the Relying Party instead of URL
1016
if (passkey && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
1021
// Additional URL check may have already inserted the entry to the list
1022
if (!entries.contains(entry)) {
1023
entries.append(entry);
1031
QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
1032
const QString& formUrl,
1033
const StringPairList& keyList,
1036
// Check if database is connected with KeePassXC-Browser. If so, return browser key (otherwise empty)
1037
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
1038
for (const StringPair& keyPair : keyList) {
1039
QString key = db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + keyPair.first);
1040
if (!key.isEmpty() && keyPair.second == key) {
1041
return keyPair.first;
1047
// Get the list of databases to search
1048
QList<QSharedPointer<Database>> databases;
1050
if (browserSettings()->searchInAllDatabases()) {
1051
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1052
auto db = dbWidget->database();
1053
auto key = databaseConnected(dbWidget->database());
1054
if (db && !key.isEmpty()) {
1060
const auto& db = getDatabase();
1061
auto key = databaseConnected(db);
1062
if (!key.isEmpty()) {
1068
// Search entries matching the hostname
1069
QString hostname = QUrl(siteUrl).host();
1070
QList<Entry*> entries;
1072
for (const auto& db : databases) {
1073
entries << searchEntries(db, siteUrl, formUrl, keys, passkey);
1075
} while (entries.isEmpty() && removeFirstDomain(hostname));
1080
QString BrowserService::decodeCustomDataRestrictKey(const QString& key)
1082
return key.isEmpty() ? tr("Disable") : key;
1085
void BrowserService::requestGlobalAutoType(const QString& search)
1087
emit osUtils->globalShortcutTriggered("autotype", search);
1090
QList<Entry*> BrowserService::sortEntries(QList<Entry*>& entries, const QString& siteUrl, const QString& formUrl)
1092
// Build map of prioritized entries
1093
QMultiMap<int, Entry*> priorities;
1094
for (auto* entry : entries) {
1095
priorities.insert(sortPriority(entry->getAllUrls(), siteUrl, formUrl), entry);
1098
auto keys = priorities.uniqueKeys();
1099
std::sort(keys.begin(), keys.end(), [](int l, int r) { return l > r; });
1101
QList<Entry*> results;
1102
for (auto key : keys) {
1103
results << priorities.values(key);
1105
if (browserSettings()->bestMatchOnly() && !results.isEmpty()) {
1106
// Early out once we find the highest batch of matches
1114
void BrowserService::allowEntry(Entry* entry, const QString& siteHost, const QString& formUrl, const QString& realm)
1116
BrowserEntryConfig config;
1118
config.allow(siteHost);
1120
if (!formUrl.isEmpty() && siteHost != formUrl) {
1121
config.allow(formUrl);
1124
if (!realm.isEmpty()) {
1125
config.setRealm(realm);
1131
void BrowserService::denyEntry(Entry* entry, const QString& siteHost, const QString& formUrl, const QString& realm)
1133
BrowserEntryConfig config;
1135
config.deny(siteHost);
1137
if (!formUrl.isEmpty() && siteHost != formUrl) {
1138
config.deny(formUrl);
1141
if (!realm.isEmpty()) {
1142
config.setRealm(realm);
1148
QJsonObject BrowserService::prepareEntry(const Entry* entry)
1151
#ifdef WITH_XC_BROWSER_PASSKEYS
1152
// Use Passkey's username instead if found
1153
res["login"] = entry->hasPasskey() ? passkeyUtils()->getUsernameFromEntry(entry)
1154
: entry->resolveMultiplePlaceholders(entry->username());
1156
res["login"] = entry->resolveMultiplePlaceholders(entry->username());
1158
res["password"] = entry->resolveMultiplePlaceholders(entry->password());
1159
res["name"] = entry->resolveMultiplePlaceholders(entry->title());
1160
res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
1161
res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
1163
if (entry->hasTotp()) {
1164
res["totp"] = entry->totp();
1167
if (entry->isExpired()) {
1168
res["expired"] = TRUE_STR;
1171
auto skipAutoSubmitGroup = entry->group()->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
1172
if (skipAutoSubmitGroup == Group::Inherit) {
1173
if (entry->customData()->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) {
1174
res["skipAutoSubmit"] = entry->customData()->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
1177
res["skipAutoSubmit"] = skipAutoSubmitGroup == Group::Enable ? TRUE_STR : FALSE_STR;
1180
if (browserSettings()->supportKphFields()) {
1181
const EntryAttributes* attr = entry->attributes();
1182
QJsonArray stringFields;
1183
for (const auto& key : attr->keys()) {
1184
if (key.startsWith("KPH: ")) {
1186
sField[key] = entry->resolveMultiplePlaceholders(attr->value(key));
1187
stringFields.append(sField);
1190
res["stringFields"] = stringFields;
1195
BrowserService::Access
1196
BrowserService::checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm)
1198
if (entry->isExpired() && !browserSettings()->allowExpiredCredentials()) {
1202
BrowserEntryConfig config;
1203
if (!config.load(entry)) {
1206
if ((config.isAllowed(siteHost)) && (formHost.isEmpty() || config.isAllowed(formHost))) {
1209
if ((config.isDenied(siteHost)) || (!formHost.isEmpty() && config.isDenied(formHost))) {
1212
if (!realm.isEmpty() && config.realm() != realm) {
1218
Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb)
1220
auto db = selectedDb ? selectedDb : getDatabase();
1225
auto* rootGroup = db->rootGroup();
1230
for (auto* g : rootGroup->groupsRecursive(true)) {
1231
if (g->name() == KEEPASSXCBROWSER_GROUP_NAME && !g->isRecycled()) {
1232
return db->rootGroup()->findGroupByUuid(g->uuid());
1236
auto* group = new Group();
1237
group->setUuid(QUuid::createUuid());
1238
group->setName(KEEPASSXCBROWSER_GROUP_NAME);
1239
group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
1240
group->setParent(rootGroup);
1244
// Returns the maximum sort priority given a set of match urls and the
1245
// extension provided site and form url.
1246
int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl)
1248
QList<int> priorityList;
1249
// NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that
1250
// by removing parts of the url that we don't match and direct matching others
1251
const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo;
1252
const auto adjustedSiteUrl = QUrl(siteUrl).adjusted(stdOpts);
1253
const auto adjustedFormUrl = QUrl(formUrl).adjusted(stdOpts);
1255
auto getPriority = [&](const QString& givenUrl) {
1256
auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts);
1258
// Default to https scheme if undefined
1259
if (url.scheme().isEmpty() || !givenUrl.contains("://")) {
1260
url.setScheme("https");
1263
// Add the empty path to the URL if it's missing.
1264
// URL's from the extension always have a path set, entry URL's can be without.
1265
if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) {
1269
// Reject invalid urls and hosts, except 'localhost', and scheme mismatch
1270
if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost")
1271
|| url.scheme() != adjustedSiteUrl.scheme()) {
1275
// Exact match with site url or form url
1276
if (url.matches(adjustedSiteUrl, QUrl::None) || url.matches(adjustedFormUrl, QUrl::None)) {
1280
// Exact match without the query string
1281
if (url.matches(adjustedSiteUrl, QUrl::RemoveQuery) || url.matches(adjustedFormUrl, QUrl::RemoveQuery)) {
1285
// Parent directory match
1286
if (url.isParentOf(adjustedSiteUrl) || url.isParentOf(adjustedFormUrl)) {
1290
// Match without path (ie, FQDN match), form url prioritizes lower than site url
1291
if (url.host() == adjustedSiteUrl.host()) {
1294
if (url.host() == adjustedFormUrl.host()) {
1298
// Site/form url ends with given url (subdomain mismatch)
1299
if (adjustedSiteUrl.host().endsWith(url.host())) {
1302
if (adjustedFormUrl.host().endsWith(url.host())) {
1306
// No valid match found
1310
for (const auto& entryUrl : urls) {
1311
priorityList << getPriority(entryUrl);
1314
return *std::max_element(priorityList.begin(), priorityList.end());
1317
bool BrowserService::removeFirstDomain(QString& hostname)
1319
int pos = hostname.indexOf(".");
1324
// Don't remove the second-level domain if it's the only one
1325
if (hostname.count(".") > 1) {
1326
hostname = hostname.mid(pos + 1);
1327
return !hostname.isEmpty();
1334
/* Test if a search URL matches a custom entry. If the URL has the schema "keepassxc", some special checks will be made.
1335
* Otherwise, this simply delegates to handleURL(). */
1336
bool BrowserService::shouldIncludeEntry(Entry* entry,
1338
const QString& submitUrl,
1339
const bool omitWwwSubdomain)
1341
// Use this special scheme to find entries by UUID
1342
if (url.startsWith("keepassxc://by-uuid/")) {
1343
return url.endsWith("by-uuid/" + entry->uuidToHex());
1344
} else if (url.startsWith("keepassxc://by-path/")) {
1345
return url.endsWith("by-path/" + entry->path());
1348
const auto allEntryUrls = entry->getAllUrls();
1349
for (const auto& entryUrl : allEntryUrls) {
1350
if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) {
1358
#ifdef WITH_XC_BROWSER_PASSKEYS
1359
// Returns all Passkey entries for the current Relying Party
1360
QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const StringPairList& keyList)
1362
QList<Entry*> entries;
1363
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
1364
if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
1372
// Returns all Passkey entries for the current Relying Party and identical user handle
1373
QList<Entry*> BrowserService::getPasskeyEntriesWithUserHandle(const QString& rpId,
1374
const QString& userId,
1375
const StringPairList& keyList)
1377
QList<Entry*> entries;
1378
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
1379
if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId
1380
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE) == userId) {
1388
// Get all entries for the site that are allowed by the server
1389
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& assertionOptions,
1390
const QString& rpId,
1391
const StringPairList& keyList)
1393
QList<Entry*> entries;
1394
const auto allowedCredentials = passkeyUtils()->getAllowedCredentialsFromAssertionOptions(assertionOptions);
1395
if (!assertionOptions["allowCredentials"].toArray().isEmpty() && allowedCredentials.isEmpty()) {
1399
for (const auto& entry : getPasskeyEntries(rpId, keyList)) {
1400
// If allowedCredentials.isEmpty() check if entry contains an extra attribute for user handle.
1401
// If that is found, the entry should be allowed.
1402
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
1403
if (allowedCredentials.contains(passkeyUtils()->getCredentialIdFromEntry(entry))
1404
|| (allowedCredentials.isEmpty()
1405
&& entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
1413
// Checks if the same user ID already exists for the current RP ID
1414
bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
1415
const QString& rpId,
1416
const StringPairList& keyList)
1419
for (const auto& cred : excludeCredentials) {
1420
allIds << cred["id"].toString();
1423
const auto passkeyEntries = getPasskeyEntries(rpId, keyList);
1424
return std::any_of(passkeyEntries.begin(), passkeyEntries.end(), [&](const auto& entry) {
1425
return allIds.contains(passkeyUtils()->getCredentialIdFromEntry(entry));
1429
QJsonObject BrowserService::getPasskeyError(int errorCode) const
1431
return QJsonObject({{"errorCode", errorCode}});
1435
bool BrowserService::handleURL(const QString& entryUrl,
1436
const QString& siteUrl,
1437
const QString& formUrl,
1438
const bool omitWwwSubdomain)
1440
if (entryUrl.isEmpty()) {
1445
if (entryUrl.contains("://")) {
1446
entryQUrl = entryUrl;
1448
entryQUrl = QUrl::fromUserInput(entryUrl);
1450
if (browserSettings()->matchUrlScheme()) {
1451
entryQUrl.setScheme("https");
1455
// Remove WWW subdomain from matching if group setting is enabled
1456
if (omitWwwSubdomain && entryQUrl.host().startsWith("www.")) {
1457
entryQUrl.setHost(entryQUrl.host().remove("www."));
1460
// Make a direct compare if a local file is used
1461
if (siteUrl.startsWith("file://")) {
1462
return entryUrl == formUrl;
1465
// URL host validation fails
1466
if (entryQUrl.host().isEmpty()) {
1470
// Match port, if used
1471
QUrl siteQUrl(siteUrl);
1472
if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
1477
if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty()
1478
&& entryQUrl.scheme().compare(siteQUrl.scheme()) != 0) {
1482
// Check for illegal characters
1483
QRegularExpression re("[<>\\^`{|}]");
1484
if (re.match(entryUrl).hasMatch()) {
1488
// Match the base domain
1489
if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) {
1493
// Match the subdomains with the limited wildcard
1494
if (siteQUrl.host().endsWith(entryQUrl.host())) {
1501
QSharedPointer<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid)
1503
if (!rootGroupUuid.isNull()) {
1504
const auto openDatabases = getOpenDatabases();
1505
for (const auto& db : openDatabases) {
1506
if (db->rootGroup()->uuid() == rootGroupUuid) {
1512
if (m_currentDatabaseWidget) {
1513
return m_currentDatabaseWidget->database();
1518
QList<QSharedPointer<Database>> BrowserService::getOpenDatabases()
1520
QList<QSharedPointer<Database>> databaseList;
1521
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1522
if (!dbWidget->isLocked()) {
1523
databaseList << dbWidget->database();
1526
return databaseList;
1529
QSharedPointer<Database> BrowserService::selectedDatabase()
1531
QList<DatabaseWidget*> databaseWidgets;
1532
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1533
// Add only open databases
1534
if (!dbWidget->isLocked()) {
1535
databaseWidgets << dbWidget;
1539
BrowserEntrySaveDialog browserEntrySaveDialog(m_currentDatabaseWidget);
1540
int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget);
1541
if (openDatabaseCount > 1) {
1542
int res = browserEntrySaveDialog.exec();
1543
if (res == QDialog::Accepted) {
1544
const auto selectedDatabase = browserEntrySaveDialog.getSelected();
1545
if (selectedDatabase.length() > 0) {
1546
int index = selectedDatabase[0]->data(Qt::UserRole).toInt();
1547
return databaseWidgets[index]->database();
1554
// Return current database
1555
return getDatabase();
1558
void BrowserService::hideWindow() const
1560
if (m_prevWindowState == WindowState::Minimized) {
1561
getMainWindow()->showMinimized();
1564
if (m_prevWindowState == WindowState::Hidden) {
1565
macUtils()->hideOwnWindow();
1567
macUtils()->raiseLastActiveWindow();
1570
if (m_prevWindowState == WindowState::Hidden) {
1571
getMainWindow()->hideWindow();
1573
getMainWindow()->lower();
1579
void BrowserService::raiseWindow(const bool force)
1581
m_prevWindowState = WindowState::Normal;
1582
if (getMainWindow()->isMinimized()) {
1583
m_prevWindowState = WindowState::Minimized;
1588
if (macUtils()->isHidden()) {
1589
m_prevWindowState = WindowState::Hidden;
1591
macUtils()->raiseOwnWindow();
1594
if (getMainWindow()->isHidden()) {
1595
m_prevWindowState = WindowState::Hidden;
1599
getMainWindow()->bringToFront();
1604
void BrowserService::updateWindowState()
1606
m_prevWindowState = WindowState::Normal;
1607
if (getMainWindow()->isMinimized()) {
1608
m_prevWindowState = WindowState::Minimized;
1611
if (macUtils()->isHidden()) {
1612
m_prevWindowState = WindowState::Hidden;
1615
if (getMainWindow()->isHidden()) {
1616
m_prevWindowState = WindowState::Hidden;
1621
void BrowserService::databaseLocked(DatabaseWidget* dbWidget)
1625
msg["action"] = QString("database-locked");
1626
m_browserHost->broadcastClientMessage(msg);
1630
void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
1633
if (m_bringToFrontRequested) {
1634
m_bringToFrontRequested = false;
1639
msg["action"] = QString("database-unlocked");
1640
m_browserHost->broadcastClientMessage(msg);
1644
void BrowserService::activeDatabaseChanged(DatabaseWidget* dbWidget)
1647
if (dbWidget->isLocked()) {
1648
databaseLocked(dbWidget);
1650
databaseUnlocked(dbWidget);
1654
m_currentDatabaseWidget = dbWidget;
1657
void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObject& message)
1659
auto clientID = message["clientID"].toString();
1660
if (clientID.isEmpty()) {
1664
// Create a new client action if we haven't seen this id yet
1665
if (!m_browserClients.contains(clientID)) {
1666
m_browserClients.insert(clientID, QSharedPointer<BrowserAction>::create());
1669
auto& action = m_browserClients.value(clientID);
1670
auto response = action->processClientMessage(socket, message);
1671
m_browserHost->sendClientMessage(socket, response);