diff --git a/COPYING b/COPYING
index 5c95bd269..5e7337ab8 100644
--- a/COPYING
+++ b/COPYING
@@ -178,6 +178,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/entry-delete.svg
share/icons/application/scalable/actions/entry-restore.svg
share/icons/application/scalable/actions/entry-edit.svg
+ share/icons/application/scalable/actions/entry-expire.svg
share/icons/application/scalable/actions/entry-new.svg
share/icons/application/scalable/actions/favicon-download.svg
share/icons/application/scalable/actions/fingerprint.svg
diff --git a/share/icons/application/scalable/actions/entry-expire.svg b/share/icons/application/scalable/actions/entry-expire.svg
new file mode 100644
index 000000000..a0a9ad53b
--- /dev/null
+++ b/share/icons/application/scalable/actions/entry-expire.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc
index ddc728ad2..38f0eb56c 100644
--- a/share/icons/icons.qrc
+++ b/share/icons/icons.qrc
@@ -40,6 +40,7 @@
application/scalable/actions/edit-clear-locationbar-rtl.svg
application/scalable/actions/entry-clone.svg
application/scalable/actions/entry-delete.svg
+ application/scalable/actions/entry-expire.svg
application/scalable/actions/entry-restore.svg
application/scalable/actions/entry-edit.svg
application/scalable/actions/entry-new.svg
diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 0ae84a241..37bbfde44 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -5836,10 +5836,6 @@ We recommend you use the AppImage available on our downloads page.
-
- Copy Password and TOTP
-
-
&XML File…
@@ -5868,10 +5864,6 @@ We recommend you use the AppImage available on our downloads page.
No Tags
-
- Toggle Show Menubar
-
-
Remove Passkey From Entry
@@ -5880,6 +5872,10 @@ We recommend you use the AppImage available on our downloads page.
Empty Recycle Bin
+
+ Toggle Show Menubar
+
+
Show Group Panel
@@ -5892,6 +5888,14 @@ We recommend you use the AppImage available on our downloads page.
Password Generator
+
+ E&xpire Entry…
+
+
+
+ Copy Password and TOTP
+
+
ManageDatabase
@@ -8833,6 +8837,13 @@ This option is deprecated, use --set-key-file instead.
Exclude from reports
+
+ Expire Entry(s)…
+
+
+
+
+
Only show entries that have a URL
@@ -8859,6 +8870,14 @@ This option is deprecated, use --set-key-file instead.
ReportsWidgetHealthcheck
+
+ Show expired entries
+
+
+
+ (Expired)
+
+
Hover over reason to show additional details. Double-click entries to edit.
@@ -8922,18 +8941,17 @@ This option is deprecated, use --set-key-file instead.
Exclude from reports
-
- Show expired entries
-
+
+ Expire Entry(s)…
+
+
+
+
Show entries that have been excluded from reports
-
- (Expired)
-
-
ReportsWidgetHibp
@@ -9032,6 +9050,13 @@ This option is deprecated, use --set-key-file instead.
Exclude from reports
+
+ Expire Entry(s)…
+
+
+
+
+
ReportsWidgetPasskeys
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index be3c9dfbd..aa5251518 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -459,6 +459,12 @@ bool Entry::willExpireInDays(int days) const
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTime().addDays(days);
}
+void Entry::expireNow()
+{
+ setExpiryTime(Clock::currentDateTimeUtc());
+ setExpires(true);
+}
+
bool Entry::isRecycled() const
{
const Database* db = database();
diff --git a/src/core/Entry.h b/src/core/Entry.h
index f5aca8fd2..eacea4da3 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -126,6 +126,7 @@ public:
bool hasTotp() const;
bool isExpired() const;
bool willExpireInDays(int days) const;
+ void expireNow();
bool isRecycled() const;
bool isAttributeReference(const QString& key) const;
bool isAttributeReferenceOf(const QString& key, const QUuid& uuid) const;
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 36cca414d..eabe4b771 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -564,6 +564,17 @@ void DatabaseWidget::setupTotp()
setupTotpDialog->open();
}
+void DatabaseWidget::expireSelectedEntries()
+{
+ const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
+ for (const auto& index : selected) {
+ auto entry = m_entryView->entryFromIndex(index);
+ if (entry) {
+ entry->expireNow();
+ }
+ }
+}
+
void DatabaseWidget::deleteSelectedEntries()
{
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index fe5ed5428..879e804b1 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -167,6 +167,7 @@ public slots:
void replaceDatabase(QSharedPointer db);
void createEntry();
void cloneEntry();
+ void expireSelectedEntries();
void deleteSelectedEntries();
void restoreSelectedEntries();
void deleteEntries(QList entries, bool confirm = true);
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 3085f96b0..1f397a482 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -143,6 +143,7 @@ MainWindow::MainWindow()
m_entryContextMenu->addSeparator();
#endif
m_entryContextMenu->addAction(m_ui->actionEntryEdit);
+ m_entryContextMenu->addAction(m_ui->actionEntryExpire);
m_entryContextMenu->addAction(m_ui->actionEntryClone);
m_entryContextMenu->addAction(m_ui->actionEntryDelete);
m_entryContextMenu->addAction(m_ui->actionEntryNew);
@@ -311,6 +312,7 @@ MainWindow::MainWindow()
// Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true);
+ m_ui->actionEntryExpire->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryRestore->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true);
@@ -407,6 +409,7 @@ MainWindow::MainWindow()
m_ui->actionEntryNew->setIcon(icons()->icon("entry-new"));
m_ui->actionEntryClone->setIcon(icons()->icon("entry-clone"));
m_ui->actionEntryEdit->setIcon(icons()->icon("entry-edit"));
+ m_ui->actionEntryExpire->setIcon(icons()->icon("entry-expire"));
m_ui->actionEntryDelete->setIcon(icons()->icon("entry-delete"));
m_ui->actionEntryRestore->setIcon(icons()->icon("entry-restore"));
m_ui->actionEntryAutoType->setIcon(icons()->icon("auto-type"));
@@ -524,8 +527,9 @@ MainWindow::MainWindow()
connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit()));
m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry()));
- m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry()));
m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit()));
+ m_actionMultiplexer.connect(m_ui->actionEntryExpire, SIGNAL(triggered()), SLOT(expireSelectedEntries()));
+ m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry()));
m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries()));
m_actionMultiplexer.connect(m_ui->actionEntryRestore, SIGNAL(triggered()), SLOT(restoreSelectedEntries()));
@@ -952,6 +956,7 @@ void MainWindow::updateMenuActionState()
m_ui->actionEntryNew->setEnabled(inDatabase && !inRecycleBin);
m_ui->actionEntryClone->setEnabled(singleEntrySelected && !inRecycleBin);
m_ui->actionEntryEdit->setEnabled(singleEntrySelected);
+ m_ui->actionEntryExpire->setEnabled(multiEntrySelected);
m_ui->actionEntryDelete->setEnabled(multiEntrySelected);
m_ui->actionEntryRestore->setVisible(multiEntrySelected && inRecycleBin);
m_ui->actionEntryRestore->setEnabled(multiEntrySelected && inRecycleBin);
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index c7a4550d9..bce6a7303 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -314,6 +314,7 @@
+
@@ -507,6 +508,14 @@
View or edit entry
+
+
+ false
+
+
+ E&xpire Entry…
+
+
&Delete Entry…
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
index 579840a24..63267d77f 100644
--- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
@@ -275,6 +275,11 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos)
});
}
+ // Create the "expire entry" menu item
+ const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
+ menu->addAction(expEntry);
+ connect(expEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::expireSelectedEntries);
+
// Create the "delete entry" menu item
const auto deleteEntry =
new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
@@ -327,6 +332,28 @@ void ReportsWidgetBrowserStatistics::saveSettings()
// Nothing to do - the tab is passive
}
+QList ReportsWidgetBrowserStatistics::getSelectedEntries()
+{
+ QList selectedEntries;
+ for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) {
+ auto row = m_modelProxy->mapToSource(index).row();
+ auto entry = m_rowToEntry[row].second;
+ if (entry) {
+ selectedEntries << entry;
+ }
+ }
+ return selectedEntries;
+}
+
+void ReportsWidgetBrowserStatistics::expireSelectedEntries()
+{
+ for (auto entry : getSelectedEntries()) {
+ entry->expireNow();
+ }
+
+ calculateBrowserStatistics();
+}
+
void ReportsWidgetBrowserStatistics::deleteSelectedEntries()
{
const auto& selectedEntries = getSelectedEntries();
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h
index 9de20086f..9b1cc7d60 100644
--- a/src/gui/reports/ReportsWidgetBrowserStatistics.h
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h
@@ -53,6 +53,8 @@ public slots:
void calculateBrowserStatistics();
void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
+ QList getSelectedEntries();
+ void expireSelectedEntries();
void deleteSelectedEntries();
void deletePluginDataFromSelectedEntries();
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
index afce4ad27..a06189e25 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.cpp
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -325,6 +325,11 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
});
}
+ // Create the "Expire entry" menu item
+ const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
+ menu->addAction(expEntry);
+ connect(expEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::expireSelectedEntries);
+
// Create the "delete entry" menu item
const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
menu->addAction(delEntry);
@@ -367,7 +372,7 @@ void ReportsWidgetHealthcheck::saveSettings()
// nothing to do - the tab is passive
}
-void ReportsWidgetHealthcheck::deleteSelectedEntries()
+QList ReportsWidgetHealthcheck::getSelectedEntries()
{
QList selectedEntries;
for (auto index : m_ui->healthcheckTableView->selectionModel()->selectedRows()) {
@@ -377,7 +382,21 @@ void ReportsWidgetHealthcheck::deleteSelectedEntries()
selectedEntries << entry;
}
}
+ return selectedEntries;
+}
+void ReportsWidgetHealthcheck::expireSelectedEntries()
+{
+ for (auto entry : getSelectedEntries()) {
+ entry->expireNow();
+ }
+
+ calculateHealth();
+}
+
+void ReportsWidgetHealthcheck::deleteSelectedEntries()
+{
+ QList selectedEntries = getSelectedEntries();
bool permanent = !m_db->metadata()->recycleBinEnabled();
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
index 21d121b00..9a46b36b1 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.h
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -53,6 +53,8 @@ public slots:
void calculateHealth();
void emitEntryActivated(const QModelIndex& index);
void customMenuRequested(QPoint);
+ QList getSelectedEntries();
+ void expireSelectedEntries();
void deleteSelectedEntries();
private:
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
index 86be3d92f..5130123eb 100644
--- a/src/gui/reports/ReportsWidgetHibp.cpp
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -374,6 +374,11 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos)
});
}
+ // Create the "Expire entry" menu item
+ const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this);
+ menu->addAction(expEntry);
+ connect(expEntry, &QAction::triggered, this, &ReportsWidgetHibp::expireSelectedEntries);
+
// Create the "delete entry" menu item
const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
menu->addAction(delEntry);
@@ -411,7 +416,7 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos)
menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
}
-void ReportsWidgetHibp::deleteSelectedEntries()
+QList ReportsWidgetHibp::getSelectedEntries()
{
QList selectedEntries;
for (auto index : m_ui->hibpTableView->selectionModel()->selectedRows()) {
@@ -421,7 +426,21 @@ void ReportsWidgetHibp::deleteSelectedEntries()
selectedEntries << entry;
}
}
+ return selectedEntries;
+}
+void ReportsWidgetHibp::expireSelectedEntries()
+{
+ for (auto entry : getSelectedEntries()) {
+ entry->expireNow();
+ }
+
+ makeHibpTable();
+}
+
+void ReportsWidgetHibp::deleteSelectedEntries()
+{
+ QList selectedEntries = getSelectedEntries();
bool permanent = !m_db->metadata()->recycleBinEnabled();
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h
index 2955358ad..fe4bb7da6 100644
--- a/src/gui/reports/ReportsWidgetHibp.h
+++ b/src/gui/reports/ReportsWidgetHibp.h
@@ -58,6 +58,8 @@ public slots:
void fetchFailed(const QString& error);
void makeHibpTable();
void customMenuRequested(QPoint);
+ QList getSelectedEntries();
+ void expireSelectedEntries();
void deleteSelectedEntries();
private: