diff --git a/COPYING b/COPYING index ed03b77f82..4e85fc078d 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 0000000000..a0a9ad53bf --- /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 a5f86b28d1..e92c98a351 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 bc9e17f226..ed4effabc9 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -5950,6 +5950,10 @@ Expect some bugs and minor issues, this version is meant for testing purposes.Import Passkey + + Remote S&ync… + + Quit Application @@ -6054,6 +6058,10 @@ Expect some bugs and minor issues, this version is meant for testing purposes.Show Password Generator + + Remove Passkey From Entry + + Perform Auto-Type: {USERNAME} @@ -6199,11 +6207,11 @@ Expect some bugs and minor issues, this version is meant for testing purposes. - Remote S&ync… + Show Group Panel - Remove Passkey From Entry + Toggle Show Group Panel @@ -6211,15 +6219,11 @@ Expect some bugs and minor issues, this version is meant for testing purposes. - Show Group Panel - - - - Toggle Show Group Panel + Password Generator - Password Generator + E&xpire Entry… @@ -9281,6 +9285,13 @@ This option is deprecated, use --set-key-file instead. Exclude from reports + + Expire Entry(s)… + + + + + Only show entries that have a URL @@ -9307,6 +9318,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. @@ -9370,18 +9389,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 @@ -9480,6 +9498,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 9587e8d67e..1acf663818 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -460,6 +460,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 749a9fe542..3fb3fbcbbc 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -126,6 +126,7 @@ class Entry : public ModifiableObject 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 b651b52850..2b4266c907 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -569,6 +569,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 5ef8709259..1e85dd312c 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -180,6 +180,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 3191b45aa1..3a19f06683 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -144,6 +144,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); @@ -276,6 +277,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); @@ -372,6 +374,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")); @@ -489,8 +492,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())); @@ -912,6 +916,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 1110b64262..c84727c3e8 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -321,6 +321,7 @@ + @@ -544,6 +545,14 @@ Ctrl+E + + + false + + + E&xpire Entry… + + &Delete Entry… diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 579840a24d..63267d77fd 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 9de20086f9..9b1cc7d602 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 a50fd1d7db..f6151dda43 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -323,6 +323,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); @@ -365,7 +370,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()) { @@ -375,7 +380,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 21d121b00b..9a46b36b1f 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 201b1010f0..a559208aaa 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 29a2c4ff1f..8e0d5e47bc 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: