Issue #1396: remove an imported digital ID (#1515)

Digital IDs added through Sign with a Digital ID (imported .p12/.pfx files
or new self-signed ones) are saved with the app settings, but nothing ever
took one off the list, so an old or mistakenly imported certificate stayed
there for good (#1396).

Sign with a Digital ID now has Remove next to Configure New Digital ID. It
acts on the selected file-based ID, asks first ("Remove "<name>" from this
list? The file itself is not deleted."), and takes the entry off the saved
list. The .p12 file stays on disk. Keychain and Windows store IDs can't be
removed here: the OS manages them and they are listed again on every
signing.

- sign_ui: PdfCraftApp::remove_digital_id refuses OS store IDs and stale
  indexes, keeps the dialog's selection on the same ID (or the first
  usable one), and goes to Configure when no IDs are left.
- SignDraft::confirm_remove holds the entry awaiting confirmation; clicking
  another row cancels the question.
- i18n: the confirmation is translated in all 21 catalogs; ATTRIBUTION.toml
  hashes updated.
- Tests: a unit test for the list, the selection and persist/restore, and a
  real-shell test that imports a .p12, cancels, then removes it.

Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
RidwanandBrandon Thomas authored and GitHub committed 2026-10-13 02:49:38 -04:00
1 parent e88dd728cb
commit 7739685796
24 files changed
+170 -21

No files matched your search

+21 -21
View File
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "28141072a01326a59e873958c20780688da68bbce6fee6d8a0598b3b2eaf64c8"
sha256 = "a05bbd3b58f64e8c2a06cb7803009c81de049716ef089fdd0abc56c7d0eb394b"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "55741bc14dd319319db883308cee6400a95cc86a11e54ddcf84921b07d7aa5b6"
sha256 = "30f26c55038c23956d79b6b85821890a55c82bc99108a0c982b679fccf918eeb"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "77a0c98f38aceee0a2b63002a0348bc10fb6982ac76fe80d020d030c640c7dfb"
sha256 = "c92d6c1b4f92fe6cc0dda954da593e7c22821f524bff77c2cdf71796ba0ea8e3"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b29359b5f0b2be96cdb9c1600187aa738d789db13d74afda26de8d7efbe71d5f"
sha256 = "2a9b1347fe5eec8cf144c315592adb2999206c91c6be3c96115fa8871d2d18b0"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "69623423479cb67f011a7725d6174385285cc1e6e7f46785d6c2cd1c403f5f71"
sha256 = "b267b363ffd9a3d0b610f14ecf505543d53ce08c0603b78306be9a70a77d15ef"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b5c78c84b6b4d418e77b1b106f88fbbe4dad8e3faf06e5d134f069d4eeaff49c"
sha256 = "6ed7cd84692db0c02b322207206f9f1b79b47825c247e6bc4ea8e8231e3bdda2"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e891fcdfa020a94d129f5f56a27a85841153311afd45c1b066895927e24468e0"
sha256 = "bc7823dca413e745370b5fb1640cfd80c8633dac959e3f9766a4e10271b323b7"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5f8bca9c22a5a7f0015b3ef4bbc173c3040f23789a145e51119e38419a3e476a"
sha256 = "a0d155d28ea5784bcd0bff539e3282e394ae74d08ee9ee2c156c3340009f74a8"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bbc72e480ee0b22da792a95643b851c0752d69543472d79c2edc7978c9afc4fb"
sha256 = "ef3e873eadfaf5367e19834dcc892487b0eae1d089b11c0913835e0e24e9bd83"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ea26a00d59bca609e44eb2d59e493da0aa41f49c3e206f3a9ba9a4d0886d263c"
sha256 = "980062f140bbfe78ea077d1bc6ff66ea856c7e267d444054319d91b9b1fb915b"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8f45e60c676b33c2c8298274c44311900364e5ed96258bf86d4cc371a84d65ad"
sha256 = "1433b66e2b40b590b6be1089d4201d6f74aac8116f67b79caca9048bfec444bf"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "527b6f96e54741387ded3d94484c6f70bd75246a83873beeaad284531cabf710"
sha256 = "fa5ecd1eb447cce1255098829a03af6909cd48fdaf3d4853c3ec322746b6f367"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c4d0cae1401ac1aae2eb50f426c9ba0361aaeef3ea56835eeada9632b0ba4101"
sha256 = "20ef278f871692830b2021996bd7038bf7ed5bfae959d8aaaddbdfb573a33d98"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "64912fc6c3e232d8a90c6b2fe476a5a383178aeba5f1d32cc27602d5e65f141b"
sha256 = "38f753d18d4bac285d8b4eeb4cf5f84e97ae5fabbe7b63502fe46c4fe9cbdbde"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f669ad8797a518ad8e849b2332043367b0e04479d4d8de03f7e565dddb53a123"
sha256 = "aae6a9f14b7b5d4ad702aaf2dc07046216ab1b4366c65215f52ff0cbb682ff04"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e57905062cc7d54185120c66435be590cd778a98b91d158fe8f40acf17636787"
sha256 = "42fe836106db7bce7a8e492b9642569df942a0695b330f8701cb85e0d6ea93ea"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "24b37ec8b7c16eee4347c3b3e45c574d339a097dc24aad2c35524c2dbf589c40"
sha256 = "8c9d7fc2e7c040b40a4efbdad38183cc6e40adc5936f8faa1b23580b9adce704"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "7d3f5fbc9016ad86441bd8b6858627f64971facc8c5bbbcb339521007669fdb1"
sha256 = "a18e085d8d9797a50d37bcb06daef2ce9675a2450d4db86c77faeeb6a66fd644"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bcfe8940ac0f1887b4b2692a7796bbe79048a414891c16159f09040948619a21"
sha256 = "448606a33ff4e953579ac38f7d614d2193b06771141b5cba005a35f6926a1d01"
[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "8982df2065edb0aaa98e7e3606521ced403a0cc6e5ff1f70ba0e272dd5dc74e5"
sha256 = "8e2d2bf1ec678cbdaf25a8c7f952b5bfb88d162a75bf8b627a742d823b131df7"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3351,4 +3351,4 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "649ead1e9be5f868571144e9964856077aa769e5bdb0b8d3c530d6bce0e29219"
sha256 = "fef63bd8be5ab43be8e48add9719b8418ddeb483f766408b34d280cdc08a3018"
+1
View File
@@ -855,6 +855,7 @@
, Expires: ، تنتهي:
No digital IDs yet. لا توجد معرّفات رقمية بعد.
Configure New Digital ID إعداد معرّف رقمي جديد
Remove "{name}" from this list? The file itself is not deleted. إزالة "{name}" من هذه القائمة؟ لن يُحذف الملف نفسه.
Continue متابعة
Configure a Digital ID for Signing إعداد معرّف رقمي للتوقيع
Use a Digital ID from a file استخدام معرّف رقمي من ملف
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -530,6 +530,7 @@
, Expires: , валиден до:
No digital IDs yet. Все още няма цифрови идентификатори.
Configure New Digital ID Конфигурирай нов цифров идентификатор
Remove "{name}" from this list? The file itself is not deleted. Да се премахне ли „{name}“ от този списък? Самият файл няма да бъде изтрит.
Continue Продължи
Configure a Digital ID for Signing Конфигуриране на цифров идентификатор за подписване
Use a Digital ID from a file Използвай цифров идентификатор от файл
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -929,6 +929,7 @@ key Space Mezerník
, Expires: , platnost do:
No digital IDs yet. Zatím žádná digitální ID.
Configure New Digital ID Nastavit nové digitální ID
Remove "{name}" from this list? The file itself is not deleted. Odebrat „{name}“ z tohoto seznamu? Samotný soubor se nesmaže.
Continue Pokračovat
Configure a Digital ID for Signing Nastavení digitálního ID pro podepisování
Use a Digital ID from a file Použít digitální ID ze souboru
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -833,6 +833,7 @@
, Expires: , läuft ab:
No digital IDs yet. Noch keine digitalen IDs.
Configure New Digital ID Neue digitale ID einrichten
Remove "{name}" from this list? The file itself is not deleted. „{name}“ aus dieser Liste entfernen? Die Datei selbst wird nicht gelöscht.
Continue Weiter
Configure a Digital ID for Signing Digitale ID zum Signieren einrichten
Use a Digital ID from a file Digitale ID aus einer Datei verwenden
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -464,6 +464,7 @@
, Expires: , Λήξη:
No digital IDs yet. Δεν υπάρχουν ακόμη ψηφιακές ταυτότητες.
Configure New Digital ID Ρύθμιση νέας ψηφιακής ταυτότητας
Remove "{name}" from this list? The file itself is not deleted. Αφαίρεση του «{name}» από αυτή τη λίστα; Το ίδιο το αρχείο δεν διαγράφεται.
Configure a Digital ID for Signing Ρύθμιση ψηφιακής ταυτότητας για υπογραφή
Use a Digital ID from a file Χρήση ψηφιακής ταυτότητας από αρχείο
Create a new Digital ID (self-signed, saved to a password-protected file) Δημιουργία νέας ψηφιακής ταυτότητας (αυτοϋπογεγραμμένης, αποθηκευμένης σε αρχείο με κωδικό πρόσβασης)
Can't render this file because it contains an unexpected character in line 467 and column 9.
+1
View File
@@ -849,6 +849,7 @@
, Expires: , caduca:
No digital IDs yet. Aún no hay ningún ID digital.
Configure New Digital ID Configurar un nuevo ID digital
Remove "{name}" from this list? The file itself is not deleted. ¿Quitar «{name}» de esta lista? El archivo en sí no se elimina.
Continue Continuar
Configure a Digital ID for Signing Configurar un ID digital para firmar
Use a Digital ID from a file Usar un ID digital de un archivo
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -829,6 +829,7 @@
, Expires: , expire :
No digital IDs yet. Aucun ID numérique pour l'instant.
Configure New Digital ID Configurer un nouvel ID numérique
Remove "{name}" from this list? The file itself is not deleted. Retirer « {name} » de cette liste ? Le fichier lui-même n’est pas supprimé.
Continue Continuer
Configure a Digital ID for Signing Configurer un ID numérique pour signer
Use a Digital ID from a file Utiliser un ID numérique provenant d'un fichier
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -826,6 +826,7 @@
, Expires: , lejár:
No digital IDs yet. Még nincs digitális azonosító.
Configure New Digital ID Új digitális azonosító beállítása
Remove "{name}" from this list? The file itself is not deleted. Eltávolítja a(z) „{name}” elemet erről a listáról? Maga a fájl nem törlődik.
Continue Tovább
Configure a Digital ID for Signing Digitális azonosító beállítása aláíráshoz
Use a Digital ID from a file Digitális azonosító használata fájlból
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -852,6 +852,7 @@
, Expires: , Scadenza:
No digital IDs yet. Ancora nessun ID digitale.
Configure New Digital ID Configura nuovo ID digitale
Remove "{name}" from this list? The file itself is not deleted. Rimuovere "{name}" da questo elenco? Il file non viene eliminato.
Continue Continua
Configure a Digital ID for Signing Configura un ID digitale per la firma
Use a Digital ID from a file Usa un ID digitale da file
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -856,6 +856,7 @@
, Expires: 、有効期限:
No digital IDs yet. デジタル ID はまだありません。
Configure New Digital ID 新しいデジタル ID を設定
Remove "{name}" from this list? The file itself is not deleted. 「{name}」をこの一覧から削除しますか?ファイル自体は削除されません。
Continue 続行
Configure a Digital ID for Signing 署名用のデジタル ID を設定
Use a Digital ID from a file ファイルのデジタル ID を使用
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -811,6 +811,7 @@
, Expires: , Жарамдылық мерзімі:
No digital IDs yet. Әзірге цифрлық ID жоқ.
Configure New Digital ID Жаңа цифрлық ID теңшеу
Remove "{name}" from this list? The file itself is not deleted. «{name}» осы тізімнен алып тасталсын ба? Файлдың өзі жойылмайды.
Continue Жалғастыру
Configure a Digital ID for Signing Қол қоюға арналған цифрлық ID теңшеу
Use a Digital ID from a file Файлдағы цифрлық ID-ді пайдалану
Can't render this file because it contains an unexpected character in line 814 and column 9.
+1
View File
@@ -390,6 +390,7 @@
Compress document structure (object streams) Suglaudinti dokumento struktūrą (objektų srautus)
Compression Glaudinimas
Configure New Digital ID Konfigūruoti naują skaitmeninį ID
Remove "{name}" from this list? The file itself is not deleted. Pašalinti „{name}“ iš šio sąrašo? Pats failas nebus ištrintas.
Confirm Password Patvirtinti slaptažodį
Conformance level: Atitikties lygis:
Continue Tęsti
Can't render this file because it contains an unexpected character in line 393 and column 9.
+1
View File
@@ -847,6 +847,7 @@
, Expires: , Wygasa:
No digital IDs yet. Brak identyfikatorów cyfrowych.
Configure New Digital ID Skonfiguruj nowy identyfikator cyfrowy
Remove "{name}" from this list? The file itself is not deleted. Usunąć „{name}” z tej listy? Sam plik nie zostanie usunięty.
Continue Kontynuuj
Configure a Digital ID for Signing Skonfiguruj identyfikator cyfrowy do podpisywania
Use a Digital ID from a file Użyj identyfikatora cyfrowego z pliku
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -484,6 +484,7 @@
, Expires: , Expira em:
No digital IDs yet. Nenhuma ID digital ainda.
Configure New Digital ID Configurar nova ID digital
Remove "{name}" from this list? The file itself is not deleted. Remover "{name}" desta lista? O arquivo em si não é excluído.
Continue Continuar
Configure a Digital ID for Signing Configurar uma ID digital para assinatura
Use a Digital ID from a file Usar uma ID digital de um arquivo
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -849,6 +849,7 @@
, Expires: , Истекает:
No digital IDs yet. Нет цифровых идентификаторов.
Configure New Digital ID Настроить новый цифровой идентификатор
Remove "{name}" from this list? The file itself is not deleted. Убрать «{name}» из этого списка? Сам файл не удаляется.
Continue Продолжить
Configure a Digital ID for Signing Настроить цифровой идентификатор для подписи
Use a Digital ID from a file Использовать цифровой идентификатор из файла
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -425,6 +425,7 @@
Compress document structure (object streams) Stisni strukturo dokumenta (tokovi objektov)
Compression Stiskanje
Configure New Digital ID Nastavi novo digitalno identiteto
Remove "{name}" from this list? The file itself is not deleted. Odstranim »{name}« s tega seznama? Sama datoteka ne bo izbrisana.
Configure a Digital ID for Signing Nastavite digitalno identiteto za podpisovanje
Confirm Password Potrdite geslo
Conformance level: Raven skladnosti:
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -828,6 +828,7 @@ comment menu Edit సవరించు
, Expires: , గడువు:
No digital IDs yet. ఇంకా డిజిటల్ ఐడీలు ఏవీ లేవు.
Configure New Digital ID కొత్త డిజిటల్ ఐడీని కాన్ఫిగర్ చేయి
Remove "{name}" from this list? The file itself is not deleted. ఈ జాబితా నుండి "{name}"ని తీసివేయాలా? ఫైల్ తొలగించబడదు.
Continue కొనసాగించు
Configure a Digital ID for Signing సంతకం కోసం డిజిటల్ ఐడీని కాన్ఫిగర్ చేయి
Use a Digital ID from a file ఫైల్‌లోని డిజిటల్ ఐడీని ఉపయోగించు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -810,6 +810,7 @@
, Expires: , Son kullanma tarihi:
No digital IDs yet. Henüz dijital kimlik yok.
Configure New Digital ID Yeni Dijital Kimlik Yapılandır
Remove "{name}" from this list? The file itself is not deleted. "{name}" bu listeden kaldırılsın mı? Dosyanın kendisi silinmez.
Continue Devam Et
Configure a Digital ID for Signing İmzalama için Dijital Kimlik Yapılandır
Use a Digital ID from a file Dosyadan Dijital Kimlik Kullan
Can't render this file because it contains an unexpected character in line 813 and column 9.
+1
View File
@@ -853,6 +853,7 @@
, Expires: , Дійсний до:
No digital IDs yet. Ще немає цифрових посвідчень.
Configure New Digital ID Налаштувати нове цифрове посвідчення
Remove "{name}" from this list? The file itself is not deleted. Прибрати «{name}» з цього списку? Сам файл не видаляється.
Continue Продовжити
Configure a Digital ID for Signing Налаштувати цифрове посвідчення для підписування
Use a Digital ID from a file Використати цифрове посвідчення з файлу
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -537,6 +537,7 @@
, Expires: ,有效期至:
No digital IDs yet. 尚无数字 ID。
Configure New Digital ID 配置新数字 ID
Remove "{name}" from this list? The file itself is not deleted. 从此列表中移除“{name}”?文件本身不会被删除。
Continue 继续
Configure a Digital ID for Signing 配置用于签名的数字 ID
Use a Digital ID from a file 使用文件中的数字 ID
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -489,6 +489,7 @@
, Expires: ,有效期至:
No digital IDs yet. 尚無數位 ID。
Configure New Digital ID 配置新數位 ID
Remove "{name}" from this list? The file itself is not deleted. 從此清單中移除「{name}」?檔案本身不會被刪除。
Continue 繼續
Configure a Digital ID for Signing 設定簽署用的數位 ID
Use a Digital ID from a file 使用檔案中的數位 ID
Can't render this file because it contains an unexpected character in line 9 and column 16.
+83
View File
@@ -111,6 +111,8 @@ pub struct SignDraft {
pub image: Option<SignatureImage>,
pub new_id: NewIdDraft,
pub error: Option<String>,
/// Sign with a Digital ID ▸ Remove was clicked for this list entry: asking to confirm.
pub confirm_remove: Option<usize>,
}
impl SignDraft {
@@ -132,6 +134,7 @@ impl SignDraft {
image: None,
new_id: NewIdDraft::default(),
error: None,
confirm_remove: None,
}
}
}
@@ -325,6 +328,28 @@ impl PdfCraftApp {
self.digital_ids.len() - 1
}
/// Take the digital ID file at `index` off the list (issue #1396). The file itself stays on
/// disk. Keychain and Windows store IDs belong to the OS and are listed again on every
/// signing, so they can't be removed here. The open dialog's selection follows the list.
pub fn remove_digital_id(&mut self, index: usize) -> bool {
if self.digital_ids.get(index).is_none_or(|e| e.path.starts_with("keychain:") || e.path.starts_with("windows:")) {
return false;
}
self.digital_ids.remove(index);
if let Some(d) = self.sign_draft.as_mut() {
d.confirm_remove = None;
d.selected = match d.selected {
Some(s) if s > index => Some(s - 1),
Some(s) if s == index => self.digital_ids.iter().position(|e| e.unusable.is_none()),
s => s,
};
if self.digital_ids.is_empty() {
d.step = SignStep::Configure;
}
}
true
}
/// Configure New Digital ID ▸ Create: a self-signed ID saved as a .p12 next to the app data
/// (or in `export_dir_override`). Returns its list index.
fn create_digital_id(&mut self) -> Result<usize, String> {
@@ -620,6 +645,7 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
}
if resp.clicked() {
d.selected = Some(i);
d.confirm_remove = None;
}
if resp.double_clicked() {
d.step = SignStep::SignAs;
@@ -630,11 +656,29 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
}
});
ui.add_space(10.0);
// Remove takes a digital ID file off the list after asking (#1396); OS key store IDs stay.
let mut remove = None;
if let Some(e) = d.confirm_remove.and_then(|i| ids.get(i)) {
ui.label(crate::i18n::fmt(tl!("Remove \"{name}\" from this list? The file itself is not deleted."), &[("name", &e.name)]));
ui.horizontal(|ui| {
if widgets::pill_button(ui, tl!("Remove"), true).clicked() {
remove = d.confirm_remove;
}
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
d.confirm_remove = None;
}
});
ui.add_space(10.0);
}
let removable = d.selected.filter(|&i| ids.get(i).is_some_and(|e| !e.path.starts_with("keychain:") && !e.path.starts_with("windows:")));
ui.horizontal(|ui| {
if widgets::pill_button(ui, tl!("Configure New Digital ID"), false).clicked() {
d.step = SignStep::Configure;
d.error = None;
}
if d.confirm_remove.is_none() && ui.add_enabled_ui(removable.is_some(), |ui| widgets::pill_button(ui, tl!("Remove"), false)).inner.clicked() {
d.confirm_remove = removable;
}
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add_enabled_ui(d.selected.is_some(), |ui| widgets::pill_button(ui, tl!("Continue"), true)).inner.clicked() {
d.step = SignStep::SignAs;
@@ -645,6 +689,9 @@ fn choose(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
}
});
});
if let Some(i) = remove {
app.remove_digital_id(i);
}
close
}
@@ -1373,4 +1420,40 @@ mod tests {
assert_eq!(mode & 0o077, 0, "the private key is not readable by others: {mode:o}");
let _ = std::fs::remove_dir_all(&dir);
}
fn listed(path: &str, name: &str) -> DigitalIdEntry {
DigitalIdEntry { path: path.into(), name: name.into(), issuer: String::new(), email: String::new(), expires: String::new(), unusable: None }
}
/// Issue #1396: an imported ID can be taken off the list (its file stays), the dialog's
/// selection follows, an OS key store ID or a stale index is refused, and the removal is saved.
#[test]
fn a_digital_id_file_is_removed_from_the_list_and_stays_removed() {
let dir = scratch();
let file = dir.join("old.p12");
std::fs::write(&file, b"not opened").unwrap();
let mut app = PdfCraftApp::new();
app.digital_ids = vec![listed("windows:abc", "Store"), listed(&file.to_string_lossy(), "Old"), listed("/ids/new.p12", "New")];
let mut draft = SignDraft::new(0, None, None, None, &app.digital_ids);
draft.selected = Some(2);
draft.confirm_remove = Some(1);
app.sign_draft = Some(draft);
assert!(!app.remove_digital_id(0), "OS key store IDs stay");
assert!(!app.remove_digital_id(9), "an index past the list is refused");
assert!(app.remove_digital_id(1));
assert_eq!(app.digital_ids.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["Store", "New"]);
assert!(file.exists(), "the file itself is not deleted");
let d = app.sign_draft.as_ref().unwrap();
assert_eq!((d.selected, d.confirm_remove, d.step), (Some(1), None, SignStep::Choose), "still New, no pending question");
// Removing the selected ID selects the first usable one; removing the last file ID
// leaves only the store ID, which isn't saved.
assert!(app.remove_digital_id(1));
assert_eq!(app.sign_draft.as_ref().unwrap().selected, Some(0));
let mut fresh = PdfCraftApp::new();
fresh.restore(&app.persist());
assert!(fresh.digital_ids.is_empty(), "the removal survives a restart: {:?}", fresh.digital_ids);
let _ = std::fs::remove_dir_all(&dir);
}
}
+45
View File
@@ -444,3 +444,48 @@ fn an_unusable_id_is_shown_with_its_reason_and_cannot_be_chosen() {
h.run_steps(3);
assert_eq!(h.state().sign_draft.as_ref().unwrap().selected, Some(1));
}
/// Issue #1396: an imported digital ID can be removed from Sign with a Digital ID, after a
/// confirmation, and its file stays where it was.
#[test]
fn an_imported_digital_id_can_be_removed() {
let dir = dir().join("remove");
std::fs::create_dir_all(&dir).unwrap();
let p12 = dir.join("signer.p12");
std::fs::copy(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12"), &p12).unwrap();
let mut h = harness(dir.clone());
assert!(h.state_mut().execute("sign.certify_invisible"));
h.run_steps(2);
{
let d = h.state_mut().sign_draft.as_mut().unwrap();
d.new_id.create = false;
d.new_id.file = p12.to_string_lossy().into_owned();
d.new_id.file_password = "test".into();
}
h.run_steps(1);
h.get_by_label("Continue").click();
h.run_steps(4);
assert_eq!(h.state().digital_ids.len(), 1);
assert_eq!(h.state().sign_draft.as_ref().map(|d| d.step), Some(SignStep::Choose));
// Remove asks first, and Cancel keeps the ID. The dialog changes size with the question;
// egui centres it from the previous frame's size, so let it settle before each click.
h.get_by_label("Remove").click();
h.run_steps(4);
h.get_by_label("Remove \"Test Signer EC\" from this list? The file itself is not deleted.");
// The question's Cancel comes before the dialog's own.
h.get_all_by_label("Cancel").next().unwrap().click();
h.run_steps(4);
assert!(h.query_by_label_contains("from this list?").is_none());
assert_eq!(h.state().digital_ids.len(), 1);
h.get_by_label("Remove").click();
h.run_steps(4);
h.get_by_label("Remove").click();
h.run_steps(3);
assert!(h.state().digital_ids.is_empty());
assert!(p12.exists(), "the file itself is not deleted");
// With no IDs left the dialog offers to configure one.
assert_eq!(h.state().sign_draft.as_ref().map(|d| d.step), Some(SignStep::Configure));
h.get_by_label("Configure a Digital ID for Signing");
}