diff --git a/README.md b/README.md
index 4cb956bf2..2b6fd0315 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# KeePassXC - KeePass Cross-platform Community Edition
[](https://travis-ci.org/keepassxreboot/keepassxc) [](https://coveralls.io/github/keepassxreboot/keepassxc)
+
## About
KeePassXC is a community fork of [KeePassX](https://www.keepassx.org/) with the goal to extend and improve it with new features and bugfixes to provide a feature-rich, fully cross-platform and modern open-source password manager.
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index 99f119f66..a2e72f7fd 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -254,6 +254,17 @@ bool Entry::isExpired() const
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc();
}
+bool Entry::hasReferences() const
+{
+ const QList keyList = EntryAttributes::DefaultAttributes;
+ for (const QString& key : keyList) {
+ if (m_attributes->isReference(key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
EntryAttributes* Entry::attributes()
{
return m_attributes;
@@ -679,20 +690,20 @@ QString Entry::resolvePlaceholder(const QString& str) const
// using format from http://keepass.info/help/base/fieldrefs.html at the time of writing,
// but supporting lookups of standard fields and references by UUID only
- QRegExp tmpRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2);
- if (tmpRegExp.indexIn(result) != -1) {
+ QRegExp* tmpRegExp = m_attributes->referenceRegExp();
+ if (tmpRegExp->indexIn(result) != -1) {
// cap(0) contains the whole reference
// cap(1) contains which field is wanted
// cap(2) contains the uuid of the referenced entry
- Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp.cap(2).toLatin1())));
+ Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp->cap(2).toLatin1())));
if (tmpRefEntry) {
// entry found, get the relevant field
- QString tmpRefField = tmpRegExp.cap(1).toLower();
- if (tmpRefField == "t") result.replace(tmpRegExp.cap(0), tmpRefEntry->title(), Qt::CaseInsensitive);
- else if (tmpRefField == "u") result.replace(tmpRegExp.cap(0), tmpRefEntry->username(), Qt::CaseInsensitive);
- else if (tmpRefField == "p") result.replace(tmpRegExp.cap(0), tmpRefEntry->password(), Qt::CaseInsensitive);
- else if (tmpRefField == "a") result.replace(tmpRegExp.cap(0), tmpRefEntry->url(), Qt::CaseInsensitive);
- else if (tmpRefField == "n") result.replace(tmpRegExp.cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive);
+ QString tmpRefField = tmpRegExp->cap(1).toLower();
+ if (tmpRefField == "t") result.replace(tmpRegExp->cap(0), tmpRefEntry->title(), Qt::CaseInsensitive);
+ else if (tmpRefField == "u") result.replace(tmpRegExp->cap(0), tmpRefEntry->username(), Qt::CaseInsensitive);
+ else if (tmpRefField == "p") result.replace(tmpRegExp->cap(0), tmpRefEntry->password(), Qt::CaseInsensitive);
+ else if (tmpRefField == "a") result.replace(tmpRegExp->cap(0), tmpRefEntry->url(), Qt::CaseInsensitive);
+ else if (tmpRefField == "n") result.replace(tmpRegExp->cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive);
}
}
diff --git a/src/core/Entry.h b/src/core/Entry.h
index d08c7217c..38ec42d4e 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -79,6 +79,7 @@ public:
QString password() const;
QString notes() const;
bool isExpired() const;
+ bool hasReferences() const;
EntryAttributes* attributes();
const EntryAttributes* attributes() const;
EntryAttachments* attachments();
diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp
index b633cae32..865e853f2 100644
--- a/src/core/EntryAttributes.cpp
+++ b/src/core/EntryAttributes.cpp
@@ -28,6 +28,7 @@ const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
EntryAttributes::EntryAttributes(QObject* parent)
: QObject(parent)
+ , m_referenceRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2)
{
clear();
}
@@ -69,6 +70,25 @@ bool EntryAttributes::isProtected(const QString& key) const
return m_protectedAttributes.contains(key);
}
+bool EntryAttributes::isReference(const QString& key) const
+{
+ if (!m_attributes.contains(key)) {
+ Q_ASSERT(false);
+ return false;
+ }
+
+ QString data = value(key);
+ if (m_referenceRegExp.indexIn(data) != -1) {
+ return true;
+ }
+ return false;
+}
+
+QRegExp* EntryAttributes::referenceRegExp()
+{
+ return &m_referenceRegExp;
+}
+
void EntryAttributes::set(const QString& key, const QString& value, bool protect)
{
bool emitModified = false;
diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h
index 211b6d483..78afe5efa 100644
--- a/src/core/EntryAttributes.h
+++ b/src/core/EntryAttributes.h
@@ -35,6 +35,8 @@ public:
QString value(const QString& key) const;
bool contains(const QString& key) const;
bool isProtected(const QString& key) const;
+ bool isReference(const QString& key) const;
+ QRegExp* referenceRegExp();
void set(const QString& key, const QString& value, bool protect = false);
void remove(const QString& key);
void rename(const QString& oldKey, const QString& newKey);
@@ -71,6 +73,7 @@ Q_SIGNALS:
private:
QMap m_attributes;
QSet m_protectedAttributes;
+ QRegExp m_referenceRegExp;
};
#endif // KEEPASSX_ENTRYATTRIBUTES_H
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 51e0bb735..a30325057 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -132,6 +132,8 @@ void EditEntryWidget::setupAdvanced()
connect(m_advancedUi->addAttributeButton, SIGNAL(clicked()), SLOT(insertAttribute()));
connect(m_advancedUi->editAttributeButton, SIGNAL(clicked()), SLOT(editCurrentAttribute()));
connect(m_advancedUi->removeAttributeButton, SIGNAL(clicked()), SLOT(removeCurrentAttribute()));
+ connect(m_advancedUi->protectAttributeButton, SIGNAL(toggled(bool)), SLOT(protectCurrentAttribute(bool)));
+ connect(m_advancedUi->revealAttributeButton, SIGNAL(clicked(bool)), SLOT(revealCurrentAttribute()));
connect(m_advancedUi->attributesView->selectionModel(),
SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(updateCurrentAttribute()));
@@ -351,6 +353,11 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore)
m_advancedUi->attributesEdit->setEnabled(false);
}
+ QList sizes = m_advancedUi->attributesSplitter->sizes();
+ sizes.replace(0, m_advancedUi->attributesSplitter->width() * 0.3);
+ sizes.replace(1, m_advancedUi->attributesSplitter->width() * 0.7);
+ m_advancedUi->attributesSplitter->setSizes(sizes);
+
IconStruct iconStruct;
iconStruct.uuid = entry->iconUuid();
iconStruct.number = entry->iconNumber();
@@ -409,7 +416,7 @@ void EditEntryWidget::saveEntry()
return;
}
- if (m_advancedUi->attributesView->currentIndex().isValid()) {
+ if (m_advancedUi->attributesView->currentIndex().isValid() && m_advancedUi->attributesEdit->isEnabled()) {
QString key = m_attributesModel->keyByIndex(m_advancedUi->attributesView->currentIndex());
m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(),
m_entryAttributes->isProtected(key));
@@ -578,46 +585,96 @@ void EditEntryWidget::removeCurrentAttribute()
QModelIndex index = m_advancedUi->attributesView->currentIndex();
if (index.isValid()) {
- m_entryAttributes->remove(m_attributesModel->keyByIndex(index));
+ if (MessageBox::question(this, tr("Confirm Remove"), tr("Are you sure you want to remove this attribute?"),
+ QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
+ m_entryAttributes->remove(m_attributesModel->keyByIndex(index));
+ }
}
}
void EditEntryWidget::updateCurrentAttribute()
{
QModelIndex newIndex = m_advancedUi->attributesView->currentIndex();
+ QString newKey = m_attributesModel->keyByIndex(newIndex);
- if (m_history) {
- if (newIndex.isValid()) {
- QString key = m_attributesModel->keyByIndex(newIndex);
- m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key));
- m_advancedUi->attributesEdit->setEnabled(true);
+ if (!m_history && m_currentAttribute != newIndex) {
+ // Save changes to the currently selected attribute if editing is enabled
+ if (m_currentAttribute.isValid() && m_advancedUi->attributesEdit->isEnabled()) {
+ QString currKey = m_attributesModel->keyByIndex(m_currentAttribute);
+ m_entryAttributes->set(currKey, m_advancedUi->attributesEdit->toPlainText(),
+ m_entryAttributes->isProtected(currKey));
+ }
+ }
+
+ displayAttribute(newIndex, m_entryAttributes->isProtected(newKey));
+
+ m_currentAttribute = newIndex;
+}
+
+void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
+{
+ // Block signals to prevent extra calls
+ m_advancedUi->protectAttributeButton->blockSignals(true);
+
+ if (index.isValid()) {
+ QString key = m_attributesModel->keyByIndex(index);
+ if (showProtected) {
+ m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press reveal to view or edit"));
+ m_advancedUi->attributesEdit->setEnabled(false);
+ m_advancedUi->revealAttributeButton->setEnabled(true);
+ m_advancedUi->protectAttributeButton->setChecked(true);
}
else {
- m_advancedUi->attributesEdit->setPlainText("");
- m_advancedUi->attributesEdit->setEnabled(false);
+ m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key));
+ m_advancedUi->attributesEdit->setEnabled(true);
+ m_advancedUi->revealAttributeButton->setEnabled(false);
+ m_advancedUi->protectAttributeButton->setChecked(false);
}
+
+ // Don't allow editing in history view
+ m_advancedUi->protectAttributeButton->setEnabled(!m_history);
+ m_advancedUi->editAttributeButton->setEnabled(!m_history);
+ m_advancedUi->removeAttributeButton->setEnabled(!m_history);
}
else {
- if (m_currentAttribute != newIndex) {
- if (m_currentAttribute.isValid()) {
- QString key = m_attributesModel->keyByIndex(m_currentAttribute);
- m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(),
- m_entryAttributes->isProtected(key));
- }
+ m_advancedUi->attributesEdit->setPlainText("");
+ m_advancedUi->attributesEdit->setEnabled(false);
+ m_advancedUi->revealAttributeButton->setEnabled(false);
+ m_advancedUi->protectAttributeButton->setChecked(false);
+ m_advancedUi->protectAttributeButton->setEnabled(false);
+ m_advancedUi->editAttributeButton->setEnabled(false);
+ m_advancedUi->removeAttributeButton->setEnabled(false);
+ }
- if (newIndex.isValid()) {
- QString key = m_attributesModel->keyByIndex(newIndex);
- m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key));
- m_advancedUi->attributesEdit->setEnabled(true);
- }
- else {
- m_advancedUi->attributesEdit->setPlainText("");
- m_advancedUi->attributesEdit->setEnabled(false);
- }
+ m_advancedUi->protectAttributeButton->blockSignals(false);
+}
- m_advancedUi->editAttributeButton->setEnabled(newIndex.isValid());
- m_advancedUi->removeAttributeButton->setEnabled(newIndex.isValid());
- m_currentAttribute = newIndex;
+void EditEntryWidget::protectCurrentAttribute(bool state)
+{
+ QModelIndex index = m_advancedUi->attributesView->currentIndex();
+ if (!m_history && index.isValid()) {
+ QString key = m_attributesModel->keyByIndex(index);
+ if (state) {
+ // Save the current text and protect the attribute
+ m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), true);
+ } else {
+ // Unprotect the current attribute value (don't save text as it is obscured)
+ m_entryAttributes->set(key, m_entryAttributes->value(key), false);
+ }
+
+ // Display the attribute
+ displayAttribute(index, state);
+ }
+}
+
+void EditEntryWidget::revealCurrentAttribute()
+{
+ if (! m_advancedUi->attributesEdit->isEnabled()) {
+ QModelIndex index = m_advancedUi->attributesView->currentIndex();
+ if (index.isValid()) {
+ QString key = m_attributesModel->keyByIndex(index);
+ m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key));
+ m_advancedUi->attributesEdit->setEnabled(true);
}
}
}
@@ -730,8 +787,13 @@ void EditEntryWidget::removeCurrentAttachment()
return;
}
- QString key = m_attachmentsModel->keyByIndex(index);
- m_entryAttachments->remove(key);
+ QMessageBox::StandardButton ans = MessageBox::question(this, tr("Confirm Remove"),
+ tr("Are you sure you want to remove this attachment?"),
+ QMessageBox::Yes | QMessageBox::No);
+ if (ans == QMessageBox::Yes) {
+ QString key = m_attachmentsModel->keyByIndex(index);
+ m_entryAttachments->remove(key);
+ }
}
void EditEntryWidget::updateAutoTypeEnabled()
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index c8045d93c..270542e8c 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -76,6 +76,8 @@ private Q_SLOTS:
void editCurrentAttribute();
void removeCurrentAttribute();
void updateCurrentAttribute();
+ void protectCurrentAttribute(bool state);
+ void revealCurrentAttribute();
void insertAttachment();
void saveCurrentAttachment();
void openAttachment(const QModelIndex& index);
@@ -110,6 +112,8 @@ private:
QMenu* createPresetsMenu();
void updateEntryData(Entry* entry) const;
+ void displayAttribute(QModelIndex index, bool showProtected);
+
Entry* m_entry;
Database* m_database;
diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui
index 61380bb90..2c7f95dde 100644
--- a/src/gui/entry/EditEntryWidgetAdvanced.ui
+++ b/src/gui/entry/EditEntryWidgetAdvanced.ui
@@ -28,19 +28,50 @@
Additional attributes
-
+
-
-
-
- -
-
-
+
+
+ Qt::Horizontal
+
+
false
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ QListView::Adjust
+
+
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+
+ 170
+ 0
+
+
+
-
-
+
-
@@ -48,16 +79,6 @@
- -
-
-
- false
-
-
- Edit
-
-
-
-
@@ -68,6 +89,16 @@
+ -
+
+
+ false
+
+
+ Edit Name
+
+
+
-
@@ -81,6 +112,35 @@
+ -
+
+
+ true
+
+
+ margin-left:50%;margin-right:50%
+
+
+ Protect
+
+
+ true
+
+
+
+ -
+
+
+ false
+
+
+ Reveal
+
+
+ false
+
+
+
@@ -103,7 +163,7 @@
-
-
+
-
@@ -172,11 +232,12 @@
attributesView
attributesEdit
addAttributeButton
- editAttributeButton
removeAttributeButton
+ editAttributeButton
attachmentsView
addAttachmentButton
removeAttachmentButton
+ openAttachmentButton
saveAttachmentButton
diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp
index d606a777e..323a55c82 100644
--- a/src/gui/entry/EntryModel.cpp
+++ b/src/gui/entry/EntryModel.cpp
@@ -19,6 +19,7 @@
#include
#include
+#include
#include "core/DatabaseIcons.h"
#include "core/Entry.h"
@@ -127,8 +128,10 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
Entry* entry = entryFromIndex(index);
+ EntryAttributes* attr = entry->attributes();
if (role == Qt::DisplayRole) {
+ QString result;
switch (index.column()) {
case ParentGroup:
if (entry->group()) {
@@ -136,11 +139,23 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
break;
case Title:
- return entry->title();
+ result = entry->resolvePlaceholder(entry->title());
+ if (attr->isReference(EntryAttributes::TitleKey)) {
+ result.prepend(tr("Ref: ","Reference abbreviation"));
+ }
+ return result;
case Username:
- return entry->username();
+ result = entry->resolvePlaceholder(entry->username());
+ if (attr->isReference(EntryAttributes::UserNameKey)) {
+ result.prepend(tr("Ref: ","Reference abbreviation"));
+ }
+ return result;
case Url:
- return entry->url();
+ result = entry->resolvePlaceholder(entry->url());
+ if (attr->isReference(EntryAttributes::URLKey)) {
+ result.prepend(tr("Ref: ","Reference abbreviation"));
+ }
+ return result;
}
}
else if (role == Qt::DecorationRole) {
@@ -166,6 +181,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
return font;
}
+ else if (role == Qt::TextColorRole) {
+ if (entry->hasReferences()) {
+ QPalette p;
+ return QVariant(p.color(QPalette::Active, QPalette::Mid));
+ }
+ }
return QVariant();
}
diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp
index d5a16ebab..e0c8bb490 100644
--- a/tests/TestEntryModel.cpp
+++ b/tests/TestEntryModel.cpp
@@ -181,6 +181,12 @@ void TestEntryModel::testAttributesModel()
QCOMPARE(spyAboutToRemove.count(), 1);
QCOMPARE(spyRemoved.count(), 1);
+ // test attribute protection
+ QString value = entryAttributes->value("2nd");
+ entryAttributes->set("2nd", value, true);
+ QVERIFY(entryAttributes->isProtected("2nd"));
+ QCOMPARE(entryAttributes->value("2nd"), value);
+
QSignalSpy spyReset(model, SIGNAL(modelReset()));
entryAttributes->clear();
model->setEntryAttributes(0);
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index 3893038ba..7df5942e8 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -242,6 +243,19 @@ void TestGui::testEditEntry()
QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit");
QTest::keyClicks(titleEdit, "_test");
+ // Test protected attributes
+ editEntryWidget->setCurrentPage(1);
+ QPlainTextEdit* attrTextEdit = editEntryWidget->findChild("attributesEdit");
+ QTest::mouseClick(editEntryWidget->findChild("addAttributeButton"), Qt::LeftButton);
+ QString attrText = "TEST TEXT";
+ QTest::keyClicks(attrTextEdit, attrText);
+ QCOMPARE(attrTextEdit->toPlainText(), attrText);
+ QTest::mouseClick(editEntryWidget->findChild("protectAttributeButton"), Qt::LeftButton);
+ QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED"));
+ QTest::mouseClick(editEntryWidget->findChild("revealAttributeButton"), Qt::LeftButton);
+ QCOMPARE(attrTextEdit->toPlainText(), attrText);
+ editEntryWidget->setCurrentPage(0);
+
// Save the edit
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);