mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-12 20:51:45 -05:00
![Janek Bevendorff](/assets/img/avatar_default.png)
Externally opened attachments are now lifecycle-managed properly. The temporary files are created with stricter permissions and entirely random names (except for the file extension) to prevent meta data leakage. When the database is closed, the files are overwritten with random data and are also more reliably deleted than before. Changes to the temporary files are monitored and the user is asked if they want to save the changes back to the database (fixes #3130). KeePassXC does not keep a lock on any of the temporary files, resolving long-standing issues with applications such as Adobe Acrobat on Windows (fixes #5950, fixes #5839). Internally, attachments are copied less. The EntryAttachmentsWidget now only references EntryAttachments instead of owning a separate copy (which used to not be cleared properly under certain circumstances).
465 lines
17 KiB
C++
465 lines
17 KiB
C++
/*
|
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
|
* Copyright (C) 2017 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 2 or (at your option)
|
|
* version 3 of the License.
|
|
*
|
|
* 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 "EntryPreviewWidget.h"
|
|
#include "ui_EntryPreviewWidget.h"
|
|
|
|
#include "Clipboard.h"
|
|
#include "Font.h"
|
|
#include "gui/Icons.h"
|
|
#if defined(WITH_XC_KEESHARE)
|
|
#include "keeshare/KeeShare.h"
|
|
#include "keeshare/KeeShareSettings.h"
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
constexpr int GeneralTabIndex = 0;
|
|
}
|
|
|
|
EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_ui(new Ui::EntryPreviewWidget())
|
|
, m_locked(false)
|
|
, m_currentEntry(nullptr)
|
|
, m_currentGroup(nullptr)
|
|
, m_selectedTabEntry(0)
|
|
, m_selectedTabGroup(0)
|
|
{
|
|
m_ui->setupUi(this);
|
|
|
|
// Entry
|
|
m_ui->entryTotpButton->setIcon(icons()->icon("chronometer"));
|
|
m_ui->entryCloseButton->setIcon(icons()->icon("dialog-close"));
|
|
m_ui->togglePasswordButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
m_ui->toggleEntryNotesButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
m_ui->toggleGroupNotesButton->setIcon(icons()->onOffIcon("password-show", true));
|
|
|
|
m_ui->entryAttachmentsWidget->setReadOnly(true);
|
|
m_ui->entryAttachmentsWidget->setButtonsVisible(false);
|
|
|
|
// Match background of read-only text edit fields with the window
|
|
m_ui->entryPasswordLabel->setBackgroundRole(QPalette::Window);
|
|
m_ui->entryUsernameLabel->setBackgroundRole(QPalette::Window);
|
|
m_ui->entryNotesTextEdit->setBackgroundRole(QPalette::Window);
|
|
m_ui->groupNotesTextEdit->setBackgroundRole(QPalette::Window);
|
|
// Align notes text with label text
|
|
m_ui->entryNotesTextEdit->document()->setDocumentMargin(0);
|
|
m_ui->groupNotesTextEdit->document()->setDocumentMargin(0);
|
|
|
|
connect(m_ui->entryUrlLabel, SIGNAL(linkActivated(QString)), SLOT(openEntryUrl()));
|
|
|
|
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpLabel, SLOT(setVisible(bool)));
|
|
connect(m_ui->entryCloseButton, SIGNAL(clicked()), SLOT(hide()));
|
|
connect(m_ui->togglePasswordButton, SIGNAL(clicked(bool)), SLOT(setPasswordVisible(bool)));
|
|
connect(m_ui->toggleEntryNotesButton, SIGNAL(clicked(bool)), SLOT(setEntryNotesVisible(bool)));
|
|
connect(m_ui->toggleGroupNotesButton, SIGNAL(clicked(bool)), SLOT(setGroupNotesVisible(bool)));
|
|
connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
|
connect(&m_totpTimer, SIGNAL(timeout()), SLOT(updateTotpLabel()));
|
|
|
|
connect(config(), &Config::changed, this, [this](Config::ConfigKey key) {
|
|
if (key == Config::GUI_HidePreviewPanel) {
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
}
|
|
});
|
|
|
|
// Group
|
|
m_ui->groupCloseButton->setIcon(icons()->icon("dialog-close"));
|
|
connect(m_ui->groupCloseButton, SIGNAL(clicked()), SLOT(hide()));
|
|
connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
|
|
|
setFocusProxy(m_ui->entryTabWidget);
|
|
|
|
#if !defined(WITH_XC_KEESHARE)
|
|
removeTab(m_ui->groupTabWidget, m_ui->groupShareTab);
|
|
#endif
|
|
}
|
|
|
|
EntryPreviewWidget::~EntryPreviewWidget()
|
|
{
|
|
}
|
|
|
|
void EntryPreviewWidget::clear()
|
|
{
|
|
hide();
|
|
m_currentEntry = nullptr;
|
|
m_currentGroup = nullptr;
|
|
m_ui->entryAttachmentsWidget->unlinkAttachments();
|
|
}
|
|
|
|
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
|
|
{
|
|
if (!selectedEntry) {
|
|
hide();
|
|
return;
|
|
}
|
|
|
|
m_currentEntry = selectedEntry;
|
|
|
|
updateEntryHeaderLine();
|
|
updateEntryTotp();
|
|
updateEntryGeneralTab();
|
|
updateEntryAdvancedTab();
|
|
updateEntryAutotypeTab();
|
|
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
|
const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
|
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
|
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
|
}
|
|
|
|
void EntryPreviewWidget::setGroup(Group* selectedGroup)
|
|
{
|
|
if (!selectedGroup) {
|
|
hide();
|
|
return;
|
|
}
|
|
|
|
m_currentGroup = selectedGroup;
|
|
updateGroupHeaderLine();
|
|
updateGroupGeneralTab();
|
|
|
|
#if defined(WITH_XC_KEESHARE)
|
|
updateGroupSharingTab();
|
|
#endif
|
|
|
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
|
const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
|
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
|
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
|
}
|
|
|
|
void EntryPreviewWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
|
{
|
|
m_locked = mode == DatabaseWidget::Mode::LockedMode;
|
|
if (m_locked) {
|
|
return;
|
|
}
|
|
|
|
if (mode == DatabaseWidget::Mode::ViewMode) {
|
|
if (m_currentGroup && m_ui->stackedWidget->currentWidget() == m_ui->pageGroup) {
|
|
setGroup(m_currentGroup);
|
|
} else if (m_currentEntry) {
|
|
setEntry(m_currentEntry);
|
|
} else {
|
|
hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryHeaderLine()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title());
|
|
m_ui->entryTitleLabel->setRawText(hierarchy(m_currentEntry->group(), title));
|
|
m_ui->entryIcon->setPixmap(m_currentEntry->iconPixmap(IconSize::Large));
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryTotp()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
const bool hasTotp = m_currentEntry->hasTotp();
|
|
m_ui->entryTotpButton->setVisible(hasTotp);
|
|
m_ui->entryTotpLabel->hide();
|
|
m_ui->entryTotpButton->setChecked(false);
|
|
|
|
if (hasTotp) {
|
|
m_totpTimer.start(1000);
|
|
updateTotpLabel();
|
|
} else {
|
|
m_ui->entryTotpLabel->clear();
|
|
m_totpTimer.stop();
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::setPasswordVisible(bool state)
|
|
{
|
|
const QString password = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password());
|
|
if (state) {
|
|
m_ui->entryPasswordLabel->setText(password);
|
|
m_ui->entryPasswordLabel->setCursorPosition(0);
|
|
m_ui->entryPasswordLabel->setFont(Font::fixedFont());
|
|
} else if (password.isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) {
|
|
m_ui->entryPasswordLabel->setText("");
|
|
} else {
|
|
m_ui->entryPasswordLabel->setText(QString("\u25cf").repeated(6));
|
|
}
|
|
m_ui->togglePasswordButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setEntryNotesVisible(bool state)
|
|
{
|
|
setNotesVisible(m_ui->entryNotesTextEdit, m_currentEntry->notes(), state);
|
|
m_ui->toggleEntryNotesButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setGroupNotesVisible(bool state)
|
|
{
|
|
setNotesVisible(m_ui->groupNotesTextEdit, m_currentGroup->notes(), state);
|
|
m_ui->toggleGroupNotesButton->setIcon(icons()->onOffIcon("password-show", state));
|
|
}
|
|
|
|
void EntryPreviewWidget::setNotesVisible(QTextEdit* notesWidget, const QString& notes, bool state)
|
|
{
|
|
if (state) {
|
|
notesWidget->setPlainText(notes);
|
|
notesWidget->moveCursor(QTextCursor::Start);
|
|
notesWidget->ensureCursorVisible();
|
|
} else {
|
|
if (!notes.isEmpty()) {
|
|
notesWidget->setPlainText(QString("\u25cf").repeated(6));
|
|
}
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryGeneralTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
m_ui->entryUsernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username()));
|
|
m_ui->entryUsernameLabel->setCursorPosition(0);
|
|
|
|
if (config()->get(Config::Security_HidePasswordPreviewPanel).toBool()) {
|
|
// Hide password
|
|
setPasswordVisible(false);
|
|
// Show the password toggle button if there are dots in the label
|
|
m_ui->togglePasswordButton->setVisible(!m_ui->entryPasswordLabel->text().isEmpty());
|
|
m_ui->togglePasswordButton->setChecked(false);
|
|
} else {
|
|
// Show password
|
|
setPasswordVisible(true);
|
|
m_ui->togglePasswordButton->setVisible(false);
|
|
}
|
|
|
|
auto hasNotes = !m_currentEntry->notes().isEmpty();
|
|
auto hideNotes = config()->get(Config::Security_HideNotes).toBool();
|
|
|
|
m_ui->entryNotesTextEdit->setVisible(hasNotes);
|
|
setEntryNotesVisible(hasNotes && !hideNotes);
|
|
m_ui->toggleEntryNotesButton->setVisible(hasNotes && hideNotes
|
|
&& !m_ui->entryNotesTextEdit->toPlainText().isEmpty());
|
|
m_ui->toggleEntryNotesButton->setChecked(false);
|
|
|
|
if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
|
|
m_ui->entryNotesTextEdit->setFont(Font::fixedFont());
|
|
} else {
|
|
m_ui->entryNotesTextEdit->setFont(Font::defaultFont());
|
|
}
|
|
|
|
m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl());
|
|
const QString url = m_currentEntry->url();
|
|
if (!url.isEmpty()) {
|
|
// URL is well formed and can be opened in a browser
|
|
m_ui->entryUrlLabel->setUrl(m_currentEntry->resolveMultiplePlaceholders(url));
|
|
m_ui->entryUrlLabel->setCursor(Qt::PointingHandCursor);
|
|
m_ui->entryUrlLabel->setOpenExternalLinks(false);
|
|
} else {
|
|
m_ui->entryUrlLabel->setUrl({});
|
|
m_ui->entryUrlLabel->setCursor(Qt::ArrowCursor);
|
|
}
|
|
|
|
const TimeInfo entryTime = m_currentEntry->timeInfo();
|
|
const QString expires =
|
|
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
|
m_ui->entryExpirationLabel->setText(expires);
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryAdvancedTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
m_ui->entryAttributesTable->clear();
|
|
|
|
const EntryAttributes* attributes = m_currentEntry->attributes();
|
|
const QStringList customAttributes = attributes->customKeys();
|
|
const bool hasAttributes = !customAttributes.isEmpty();
|
|
const bool hasAttachments = !m_currentEntry->attachments()->isEmpty();
|
|
m_ui->entryAttributesTable->setRowCount(customAttributes.size());
|
|
m_ui->entryAttributesTable->setColumnCount(3);
|
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAdvancedTab, hasAttributes || hasAttachments);
|
|
if (hasAttributes) {
|
|
auto i = 0;
|
|
QFont font;
|
|
font.setBold(true);
|
|
for (const QString& key : customAttributes) {
|
|
m_ui->entryAttributesTable->setItem(i, 0, new QTableWidgetItem(key));
|
|
|
|
if (attributes->isProtected(key)) {
|
|
// only show the reveal button on protected attributes
|
|
auto button = new QToolButton();
|
|
button->setCheckable(true);
|
|
button->setChecked(false);
|
|
button->setIcon(icons()->onOffIcon("password-show", false));
|
|
button->setProperty("value", attributes->value(key));
|
|
button->setProperty("row", i);
|
|
m_ui->entryAttributesTable->setCellWidget(i, 1, button);
|
|
m_ui->entryAttributesTable->setItem(i, 2, new QTableWidgetItem(QString("\u25cf").repeated(6)));
|
|
|
|
connect(button, &QToolButton::clicked, this, [this](bool state) {
|
|
auto btn = qobject_cast<QToolButton*>(sender());
|
|
btn->setIcon(icons()->onOffIcon("password-show", state));
|
|
auto row = btn->property("row").toInt();
|
|
if (state) {
|
|
m_ui->entryAttributesTable->item(row, 2)->setText(btn->property("value").toString());
|
|
} else {
|
|
m_ui->entryAttributesTable->item(row, 2)->setText(QString("\u25cf").repeated(6));
|
|
}
|
|
// Maintain button height while showing contents of cell
|
|
auto size = btn->size();
|
|
m_ui->entryAttributesTable->resizeRowToContents(row);
|
|
btn->setFixedSize(size);
|
|
});
|
|
} else {
|
|
m_ui->entryAttributesTable->setItem(i, 2, new QTableWidgetItem(attributes->value(key)));
|
|
}
|
|
|
|
m_ui->entryAttributesTable->item(i, 0)->setFont(font);
|
|
m_ui->entryAttributesTable->item(i, 0)->setTextAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
m_ui->entryAttributesTable->item(i, 2)->setTextAlignment(Qt::AlignTop | Qt::AlignLeft);
|
|
|
|
++i;
|
|
}
|
|
connect(m_ui->entryAttributesTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int column) {
|
|
if (column == 2) {
|
|
clipboard()->setText(m_ui->entryAttributesTable->item(row, column)->text());
|
|
}
|
|
});
|
|
}
|
|
|
|
m_ui->entryAttributesTable->horizontalHeader()->setStretchLastSection(true);
|
|
m_ui->entryAttributesTable->resizeColumnsToContents();
|
|
m_ui->entryAttributesTable->resizeRowsToContents();
|
|
m_ui->entryAttachmentsWidget->linkAttachments(m_currentEntry->attachments());
|
|
}
|
|
|
|
void EntryPreviewWidget::updateEntryAutotypeTab()
|
|
{
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
m_ui->entrySequenceLabel->setText(m_currentEntry->effectiveAutoTypeSequence());
|
|
m_ui->entryAutotypeTree->clear();
|
|
QList<QTreeWidgetItem*> items;
|
|
const AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations();
|
|
const auto associations = autotypeAssociations->getAll();
|
|
for (const auto& assoc : associations) {
|
|
const QString sequence =
|
|
assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence() : assoc.sequence;
|
|
items.append(new QTreeWidgetItem(m_ui->entryAutotypeTree, {assoc.window, sequence}));
|
|
}
|
|
|
|
m_ui->entryAutotypeTree->addTopLevelItems(items);
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, m_currentEntry->autoTypeEnabled());
|
|
}
|
|
|
|
void EntryPreviewWidget::updateGroupHeaderLine()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
m_ui->groupTitleLabel->setRawText(hierarchy(m_currentGroup, {}));
|
|
m_ui->groupIcon->setPixmap(m_currentGroup->iconPixmap(IconSize::Large));
|
|
}
|
|
|
|
void EntryPreviewWidget::updateGroupGeneralTab()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
const QString searchingText = m_currentGroup->resolveSearchingEnabled() ? tr("Enabled") : tr("Disabled");
|
|
m_ui->groupSearchingLabel->setText(searchingText);
|
|
|
|
const QString autotypeText = m_currentGroup->resolveAutoTypeEnabled() ? tr("Enabled") : tr("Disabled");
|
|
m_ui->groupAutotypeLabel->setText(autotypeText);
|
|
|
|
const TimeInfo groupTime = m_currentGroup->timeInfo();
|
|
const QString expiresText =
|
|
groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
|
m_ui->groupExpirationLabel->setText(expiresText);
|
|
|
|
if (config()->get(Config::Security_HideNotes).toBool()) {
|
|
setGroupNotesVisible(false);
|
|
m_ui->toggleGroupNotesButton->setVisible(!m_ui->groupNotesTextEdit->toPlainText().isEmpty());
|
|
m_ui->toggleGroupNotesButton->setChecked(false);
|
|
} else {
|
|
setGroupNotesVisible(true);
|
|
m_ui->toggleGroupNotesButton->setVisible(false);
|
|
}
|
|
|
|
if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
|
|
m_ui->groupNotesTextEdit->setFont(Font::fixedFont());
|
|
} else {
|
|
m_ui->groupNotesTextEdit->setFont(Font::defaultFont());
|
|
}
|
|
}
|
|
|
|
#if defined(WITH_XC_KEESHARE)
|
|
void EntryPreviewWidget::updateGroupSharingTab()
|
|
{
|
|
Q_ASSERT(m_currentGroup);
|
|
setTabEnabled(m_ui->groupTabWidget, m_ui->groupShareTab, KeeShare::isShared(m_currentGroup));
|
|
auto reference = KeeShare::referenceOf(m_currentGroup);
|
|
m_ui->groupShareTypeLabel->setText(KeeShare::referenceTypeLabel(reference));
|
|
m_ui->groupSharePathLabel->setText(reference.path);
|
|
}
|
|
#endif
|
|
|
|
void EntryPreviewWidget::updateTotpLabel()
|
|
{
|
|
if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
|
|
const QString totpCode = m_currentEntry->totp();
|
|
const QString firstHalf = totpCode.left(totpCode.size() / 2);
|
|
const QString secondHalf = totpCode.mid(totpCode.size() / 2);
|
|
m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf);
|
|
} else {
|
|
m_ui->entryTotpLabel->clear();
|
|
m_totpTimer.stop();
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::updateTabIndexes()
|
|
{
|
|
m_selectedTabEntry = m_ui->entryTabWidget->currentIndex();
|
|
m_selectedTabGroup = m_ui->groupTabWidget->currentIndex();
|
|
}
|
|
|
|
void EntryPreviewWidget::openEntryUrl()
|
|
{
|
|
if (m_currentEntry) {
|
|
emit entryUrlActivated(m_currentEntry);
|
|
}
|
|
}
|
|
|
|
void EntryPreviewWidget::removeTab(QTabWidget* tabWidget, QWidget* widget)
|
|
{
|
|
const int tabIndex = tabWidget->indexOf(widget);
|
|
Q_ASSERT(tabIndex != -1);
|
|
tabWidget->removeTab(tabIndex);
|
|
}
|
|
|
|
void EntryPreviewWidget::setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled)
|
|
{
|
|
const int tabIndex = tabWidget->indexOf(widget);
|
|
Q_ASSERT(tabIndex != -1);
|
|
tabWidget->setTabEnabled(tabIndex, enabled);
|
|
}
|
|
|
|
QString EntryPreviewWidget::hierarchy(const Group* group, const QString& title)
|
|
{
|
|
QString groupList = QString("%1").arg(group->hierarchy().join(" / "));
|
|
return title.isEmpty() ? groupList : QString("%1 / %2").arg(groupList, title);
|
|
}
|