diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c8929aef..a45933562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,8 +245,8 @@ if(MINGW) set(CMAKE_RC_COMPILE_OBJECT " -O coff -i -o ") if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) # Enable DEP and ASLR - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va") + set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va") endif() endif() diff --git a/README.md b/README.md index 9f81795c5..b09a5d208 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ so please check out your distribution's package list to see if KeePassXC is avai [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepasshttp-connector/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and [passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#Note_about_KeePassHTTP) -- Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/iopaggbpplllidnfmcghoonnokmjoicf) +- Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk) - Many bug fixes For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document. diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index aa8064bac..3f44a9944 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -140,13 +140,6 @@ QStringList AutoType::windowTitles() return m_plugin->windowTitles(); } -void AutoType::resetInAutoType() -{ - m_inAutoType.unlock(); - - emit autotypeRejected(); -} - void AutoType::raiseWindow() { #if defined(Q_OS_MAC) @@ -199,9 +192,14 @@ int AutoType::callEventFilter(void* event) */ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, const QString& sequence, WId window) { + if (!m_inAutoType.tryLock()) { + return; + } + // no edit to the sequence beyond this point if (!verifyAutoTypeSyntax(sequence)) { emit autotypeRejected(); + m_inAutoType.unlock(); return; } @@ -210,6 +208,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c if (!parseActions(sequence, entry, actions)) { emit autotypeRejected(); + m_inAutoType.unlock(); return; } @@ -233,6 +232,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c if (m_plugin->activeWindow() != window) { qWarning("Active window changed, interrupting auto-type."); emit autotypeRejected(); + m_inAutoType.unlock(); return; } @@ -242,6 +242,8 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c // emit signal only if autotype performed correctly emit autotypePerformed(); + + m_inAutoType.unlock(); } /** @@ -259,13 +261,7 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow) return; } - if (!m_inAutoType.tryLock()) { - return; - } - executeAutoTypeActions(entry, hideWindow, sequences.first()); - - m_inAutoType.unlock(); } /** @@ -278,13 +274,14 @@ void AutoType::performGlobalAutoType(const QList& dbList) return; } - QString windowTitle = m_plugin->activeWindowTitle(); - - if (windowTitle.isEmpty()) { + if (!m_inGlobalAutoTypeDialog.tryLock()) { return; } - if (!m_inAutoType.tryLock()) { + QString windowTitle = m_plugin->activeWindowTitle(); + + if (windowTitle.isEmpty()) { + m_inGlobalAutoTypeDialog.unlock(); return; } @@ -303,8 +300,6 @@ void AutoType::performGlobalAutoType(const QList& dbList) } if (matchList.isEmpty()) { - m_inAutoType.unlock(); - if (qobject_cast(QCoreApplication::instance())) { auto* msgBox = new QMessageBox(); msgBox->setAttribute(Qt::WA_DeleteOnClose); @@ -318,16 +313,20 @@ void AutoType::performGlobalAutoType(const QList& dbList) msgBox->activateWindow(); } + m_inGlobalAutoTypeDialog.unlock(); emit autotypeRejected(); } else if ((matchList.size() == 1) && !config()->get("security/autotypeask").toBool()) { executeAutoTypeActions(matchList.first().entry, nullptr, matchList.first().sequence); - m_inAutoType.unlock(); + m_inGlobalAutoTypeDialog.unlock(); } else { m_windowFromGlobal = m_plugin->activeWindow(); auto* selectDialog = new AutoTypeSelectDialog(); + + // connect slots, both of which must unlock the m_inGlobalAutoTypeDialog mutex connect(selectDialog, SIGNAL(matchActivated(AutoTypeMatch)), SLOT(performAutoTypeFromGlobal(AutoTypeMatch))); - connect(selectDialog, SIGNAL(rejected()), SLOT(resetInAutoType())); + connect(selectDialog, SIGNAL(rejected()), SLOT(autoTypeRejectedFromGlobal())); + selectDialog->setMatchList(matchList); #if defined(Q_OS_MAC) m_plugin->raiseOwnWindow(); @@ -341,14 +340,22 @@ void AutoType::performGlobalAutoType(const QList& dbList) void AutoType::performAutoTypeFromGlobal(AutoTypeMatch match) { - // We don't care about the result here, the mutex should already be locked. Now it's locked for sure - m_inAutoType.tryLock(); - m_plugin->raiseWindow(m_windowFromGlobal); - executeAutoTypeActions(match.entry, nullptr, match.sequence, m_windowFromGlobal); - m_inAutoType.unlock(); + // make sure the mutex is definitely locked before we unlock it + Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock()); + m_inGlobalAutoTypeDialog.unlock(); +} + +void AutoType::autoTypeRejectedFromGlobal() +{ + // this slot can be called twice when the selection dialog is deleted, + // so make sure the mutex is locked before we try unlocking it + Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock()); + m_inGlobalAutoTypeDialog.unlock(); + + emit autotypeRejected(); } /** diff --git a/src/autotype/AutoType.h b/src/autotype/AutoType.h index 98a7bd7fa..55adac7d1 100644 --- a/src/autotype/AutoType.h +++ b/src/autotype/AutoType.h @@ -69,7 +69,7 @@ signals: private slots: void performAutoTypeFromGlobal(AutoTypeMatch match); - void resetInAutoType(); + void autoTypeRejectedFromGlobal(); void unloadPlugin(); private: @@ -88,6 +88,7 @@ private: bool windowMatches(const QString& windowTitle, const QString& windowPattern); QMutex m_inAutoType; + QMutex m_inGlobalAutoTypeDialog; int m_autoTypeDelay; Qt::Key m_currentGlobalKey; Qt::KeyboardModifiers m_currentGlobalModifiers; diff --git a/src/browser/NativeMessagingBase.cpp b/src/browser/NativeMessagingBase.cpp index 743953e95..f61a36045 100644 --- a/src/browser/NativeMessagingBase.cpp +++ b/src/browser/NativeMessagingBase.cpp @@ -121,7 +121,8 @@ void NativeMessagingBase::sendReply(const QJsonObject& json) void NativeMessagingBase::sendReply(const QString& reply) { if (!reply.isEmpty()) { - uint len = reply.length(); + QByteArray bytes = reply.toUtf8(); + uint len = bytes.size(); std::cout << char(((len>>0) & 0xFF)) << char(((len>>8) & 0xFF)) << char(((len>>16) & 0xFF)) << char(((len>>24) & 0xFF)); std::cout << reply.toStdString() << std::flush; } diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 330542eb9..c6f1907ad 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -97,6 +97,15 @@ QString FilePath::wordlistPath(const QString& name) } QIcon FilePath::applicationIcon() +{ +#ifdef KEEPASSXC_DIST_SNAP + return icon("apps", "keepassxc", false); +#else + return icon("apps", "keepassxc"); +#endif +} + +QIcon FilePath::trayIcon() { bool darkIcon = useDarkIcon(); @@ -107,7 +116,6 @@ QIcon FilePath::applicationIcon() #endif } - QIcon FilePath::trayIconLocked() { #ifdef KEEPASSXC_DIST_SNAP diff --git a/src/core/FilePath.h b/src/core/FilePath.h index f84f84e0e..b0f0397e2 100644 --- a/src/core/FilePath.h +++ b/src/core/FilePath.h @@ -29,6 +29,7 @@ public: QString pluginPath(const QString& name); QString wordlistPath(const QString& name); QIcon applicationIcon(); + QIcon trayIcon(); QIcon trayIconLocked(); QIcon trayIconUnlocked(); QIcon icon(const QString& category, const QString& name, bool fromTheme = true); diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index a93110234..16792e78a 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -173,6 +173,9 @@ void DatabaseOpenWidget::openDatabase() return; } + m_ui->editPassword->setShowPassword(false); + QCoreApplication::processEvents(); + QFile file(m_filename); if (!file.open(QIODevice::ReadOnly)) { m_ui->messageWidget->showMessage( diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 2e0595449..1465cf872 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -899,7 +899,7 @@ void MainWindow::updateTrayIcon() m_trayIcon->setContextMenu(menu); - m_trayIcon->setIcon(filePath()->applicationIcon()); + m_trayIcon->setIcon(filePath()->trayIcon()); m_trayIcon->show(); } if (m_ui->tabWidget->hasLockableDatabases()) { @@ -971,7 +971,11 @@ void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) void MainWindow::hideWindow() { saveWindowInformation(); -#ifndef Q_OS_MAC +#if !defined(Q_OS_LINUX) && !defined(Q_OS_MAC) + // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at + // the same time (which would happen if both minimize on startup and minimize to tray are set) + // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. + // TODO: Add an explanation for why this is also not done on Mac (or remove the check) setWindowState(windowState() | Qt::WindowMinimized); #endif QTimer::singleShot(0, this, SLOT(hide())); @@ -983,7 +987,7 @@ void MainWindow::hideWindow() void MainWindow::toggleWindow() { - if ((QApplication::activeWindow() == this) && isVisible() && !isMinimized()) { + if (isVisible() && !isMinimized()) { hideWindow(); } else { bringToFront(); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 4759af7aa..95ce7ee6a 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -281,6 +281,8 @@ void EditEntryWidget::setupSSHAgent() connect(m_sshAgentUi->decryptButton, SIGNAL(clicked()), SLOT(decryptPrivateKey())); connect(m_sshAgentUi->copyToClipboardButton, SIGNAL(clicked()), SLOT(copyPublicKey())); + connect(m_advancedUi->attachmentsWidget->entryAttachments(), SIGNAL(modified()), SLOT(updateAttachments())); + addPage(tr("SSH Agent"), FilePath::instance()->icon("apps", "utilities-terminal"), m_sshAgentWidget); } @@ -299,6 +301,27 @@ void EditEntryWidget::updateSSHAgent() m_sshAgentUi->removeFromAgentButton->setEnabled(false); m_sshAgentUi->copyToClipboardButton->setEnabled(false); + m_sshAgentSettings = settings; + updateSSHAgentAttachments(); + + if (settings.selectedType() == "attachment") { + m_sshAgentUi->attachmentRadioButton->setChecked(true); + } else { + m_sshAgentUi->externalFileRadioButton->setChecked(true); + } + + updateSSHAgentKeyInfo(); +} + +void EditEntryWidget::updateSSHAgentAttachment() +{ + m_sshAgentUi->attachmentRadioButton->setChecked(true); + updateSSHAgentKeyInfo(); +} + +void EditEntryWidget::updateSSHAgentAttachments() +{ + m_sshAgentUi->attachmentComboBox->clear(); m_sshAgentUi->attachmentComboBox->addItem(""); auto attachments = m_advancedUi->attachmentsWidget->entryAttachments(); @@ -310,24 +333,8 @@ void EditEntryWidget::updateSSHAgent() m_sshAgentUi->attachmentComboBox->addItem(fileName); } - m_sshAgentUi->attachmentComboBox->setCurrentText(settings.attachmentName()); - m_sshAgentUi->externalFileEdit->setText(settings.fileName()); - - if (settings.selectedType() == "attachment") { - m_sshAgentUi->attachmentRadioButton->setChecked(true); - } else { - m_sshAgentUi->externalFileRadioButton->setChecked(true); - } - - m_sshAgentSettings = settings; - - updateSSHAgentKeyInfo(); -} - -void EditEntryWidget::updateSSHAgentAttachment() -{ - m_sshAgentUi->attachmentRadioButton->setChecked(true); - updateSSHAgentKeyInfo(); + m_sshAgentUi->attachmentComboBox->setCurrentText(m_sshAgentSettings.attachmentName()); + m_sshAgentUi->externalFileEdit->setText(m_sshAgentSettings.fileName()); } void EditEntryWidget::updateSSHAgentKeyInfo() @@ -420,12 +427,16 @@ void EditEntryWidget::browsePrivateKey() bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt) { + QString fileName; QByteArray privateKeyData; if (m_sshAgentUi->attachmentRadioButton->isChecked()) { - privateKeyData = m_advancedUi->attachmentsWidget->getAttachment(m_sshAgentUi->attachmentComboBox->currentText()); + fileName = m_sshAgentUi->attachmentComboBox->currentText(); + privateKeyData = m_advancedUi->attachmentsWidget->getAttachment(fileName); } else { QFile localFile(m_sshAgentUi->externalFileEdit->text()); + QFileInfo localFileInfo(localFile); + fileName = localFileInfo.fileName(); if (localFile.fileName().isEmpty()) { return false; @@ -464,6 +475,10 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt) key.setComment(m_entry->username()); } + if (key.comment().isEmpty()) { + key.setComment(fileName); + } + return true; } diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index a7c8e3271..9b2a919c6 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -103,6 +103,7 @@ private slots: #ifdef WITH_XC_SSHAGENT void updateSSHAgent(); void updateSSHAgentAttachment(); + void updateSSHAgentAttachments(); void updateSSHAgentKeyInfo(); void browsePrivateKey(); void addKeyToAgent(); diff --git a/src/main.cpp b/src/main.cpp index a7fd2d762..b3b607f25 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,8 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" +#include "cli/Utils.h" + #if defined(WITH_ASAN) && defined(WITH_LSAN) #include #endif @@ -125,7 +127,14 @@ int main(int argc, char** argv) // start minimized if configured bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool(); bool minimizeToTray = config()->get("GUI/MinimizeToTray").toBool(); +#ifndef Q_OS_LINUX if (minimizeOnStartup) { +#else + // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at + // the same time (which would happen if both minimize on startup and minimize to tray are set) + // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. + if (minimizeOnStartup && !minimizeToTray) { +#endif mainWindow.setWindowState(Qt::WindowMinimized); } if (!(minimizeOnStartup && minimizeToTray)) { @@ -148,7 +157,9 @@ int main(int argc, char** argv) // we always need consume a line of STDIN if --pw-stdin is set to clear out the // buffer for native messaging, even if the specified file does not exist static QTextStream in(stdin, QIODevice::ReadOnly); - password = in.readLine(); + static QTextStream out(stdout, QIODevice::WriteOnly); + out << QCoreApplication::translate("Main", "Database password: ") << flush; + password = Utils::getPassword(); } if (!filename.isEmpty() && QFile::exists(filename) && !filename.endsWith(".json", Qt::CaseInsensitive)) { diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 45d774aab..973e03054 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -186,7 +186,17 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, quint32 lifetime, bool confirm) } if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_SUCCESS) { - m_error = tr("Agent refused this identity."); + m_error = tr("Agent refused this identity. Possible reasons include:") + + "\n" + tr("The key has already been added."); + + if (lifetime > 0) { + m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options)."); + } + + if (confirm) { + m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); + } + return false; } @@ -268,10 +278,15 @@ void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) } QByteArray keyData; + QString fileName; if (settings.selectedType() == "attachment") { - keyData = e->attachments()->value(settings.attachmentName()); + fileName = settings.attachmentName(); + keyData = e->attachments()->value(fileName); } else if (!settings.fileName().isEmpty()) { QFile file(settings.fileName()); + QFileInfo fileInfo(file); + + fileName = fileInfo.fileName(); if (file.size() > 1024 * 1024) { continue; @@ -302,6 +317,10 @@ void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) key.setComment(e->username()); } + if (key.comment().isEmpty()) { + key.setComment(fileName); + } + if (settings.removeAtDatabaseClose()) { removeIdentityAtLock(key, uuid); } diff --git a/src/sshagent/blf.h b/src/sshagent/blf.h index 4878e5588..f1ac5a5c2 100644 --- a/src/sshagent/blf.h +++ b/src/sshagent/blf.h @@ -34,17 +34,7 @@ #ifndef _BLF_H_ #define _BLF_H_ -#ifdef _WIN32 - -#include - -typedef uint32_t u_int32_t; -typedef uint16_t u_int16_t; -typedef uint8_t u_int8_t; - -#define bzero(p,s) memset(p, 0, s) - -#endif +#include "includes.h" #if !defined(HAVE_BCRYPT_PBKDF) && !defined(HAVE_BLH_H) diff --git a/src/sshagent/blowfish.c b/src/sshagent/blowfish.c index 02e9ac0bd..e10f7e7d9 100644 --- a/src/sshagent/blowfish.c +++ b/src/sshagent/blowfish.c @@ -39,7 +39,7 @@ * Bruce Schneier. */ -#define HAVE_BLF_H +#include "includes.h" #if !defined(HAVE_BCRYPT_PBKDF) && (!defined(HAVE_BLOWFISH_INITSTATE) || \ !defined(HAVE_BLOWFISH_EXPAND0STATE) || !defined(HAVE_BLF_ENC)) @@ -51,7 +51,7 @@ #include #ifdef HAVE_BLF_H -#include "blf.h" +#include #endif #undef inline diff --git a/src/sshagent/includes.h b/src/sshagent/includes.h new file mode 100644 index 000000000..c6bb4d32e --- /dev/null +++ b/src/sshagent/includes.h @@ -0,0 +1,20 @@ +// mimic openSSH-portable's includes.h file to be able to use +// its unmodified blowfish code + +#define HAVE_BLF_H + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* activate extra prototypes for glibc */ +#endif +#include + + +#ifdef _WIN32 +#include + +typedef uint32_t u_int32_t; +typedef uint16_t u_int16_t; +typedef uint8_t u_int8_t; + +#define bzero(p,s) memset(p, 0, s) +#endif diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index b2ccd332d..54203c284 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -71,6 +71,8 @@ void TestGui::initTestCase() Config::createTempFileInstance(); // Disable autosave so we can test the modified file indicator Config::instance()->set("AutoSaveAfterEveryChange", false); + // Enable the tray icon so we can test hiding/restoring the window + Config::instance()->set("GUI/ShowTrayIcon", true); m_mainWindow = new MainWindow(); m_tabWidget = m_mainWindow->findChild("tabWidget"); @@ -1091,6 +1093,32 @@ void TestGui::testDragAndDropKdbxFiles() QCOMPARE(m_tabWidget->count(), openedDatabasesCount); } +void TestGui::testTrayRestoreHide() +{ + if (!QSystemTrayIcon::isSystemTrayAvailable()) { + QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test..."); + } + + QSystemTrayIcon* trayIcon = m_mainWindow->findChild(); + QVERIFY(m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::Trigger); + Tools::wait(100); + QVERIFY(!m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::Trigger); + Tools::wait(100); + QVERIFY(m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::Trigger); + Tools::wait(100); + QVERIFY(!m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::Trigger); + Tools::wait(100); + QVERIFY(m_mainWindow->isVisible()); +} + void TestGui::cleanupTestCase() { delete m_mainWindow; diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 0b403731d..e7232ccab 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -63,6 +63,7 @@ private slots: void testKeePass1Import(); void testDatabaseLocking(); void testDragAndDropKdbxFiles(); + void testTrayRestoreHide(); private: int addCannedEntries();