From 46c1b7857733770de4b9ebc73023d46c9e8f8aa4 Mon Sep 17 00:00:00 2001 From: Christian Hohnstaedt Date: Fri, 13 Sep 2024 14:44:51 +0200 Subject: [PATCH] Close #88: Export to multiple files Extend the ExportFile dialog by a checkbox to switch between "all-in-one-file" or every item in a separate file. It is only visible if more than one file is selected. Activating the checkbox will: - Change the file-input into a directory-input and cut off the file name - Disabling all formats that are intended for multiple items. De-activating the checkbox will: - Change the directory-input back into a file-input and append the previously erased file name again. - Disabling all export-formats that are only useful for single items. --- lib/db_base.cpp | 4 --- lib/func.cpp | 6 +++- lib/pki_export.cpp | 10 +++---- test/export.cpp | 4 +-- ui/ExportDialog.ui | 14 +++++++-- widgets/ExportDialog.cpp | 62 +++++++++++++++++++++++++++++++--------- widgets/ExportDialog.h | 5 +++- widgets/XcaTreeView.cpp | 49 ++++++++++++++++++++++++++----- widgets/XcaTreeView.h | 2 ++ 9 files changed, 120 insertions(+), 36 deletions(-) diff --git a/lib/db_base.cpp b/lib/db_base.cpp index 10d38d78..77b8ceaf 100644 --- a/lib/db_base.cpp +++ b/lib/db_base.cpp @@ -718,10 +718,6 @@ int db_base::exportFlags(const QModelIndexList &indexes) const int disabled_flags = 0; foreach(const QModelIndex &idx, indexes) disabled_flags |= exportFlags(idx); - if (indexes.size() > 1) - disabled_flags |= F_SINGLE; - else - disabled_flags |= F_MULTI; return disabled_flags; } diff --git a/lib/func.cpp b/lib/func.cpp index dcf9414b..5f244368 100644 --- a/lib/func.cpp +++ b/lib/func.cpp @@ -384,5 +384,9 @@ QString fingerprint(const QByteArray &data, const EVP_MD *type) void update_workingdir(const QString &file) { - Settings["workingdir"] = QFileInfo(file).absolutePath(); + QFileInfo fi(file); + if (fi.isDir()) + Settings["workingdir"] = fi.absoluteFilePath(); + else + Settings["workingdir"] = fi.absolutePath(); } diff --git a/lib/pki_export.cpp b/lib/pki_export.cpp index 6b4eda77..2ce5a66b 100644 --- a/lib/pki_export.cpp +++ b/lib/pki_export.cpp @@ -62,16 +62,16 @@ void pki_export::init_elements() elements = QList { new pki_export( 1, x509, "crt", "PEM", F_PEM | F_USUAL | F_SINGLE | F_CLIPBOARD, tr("PEM Text format with headers")), -new pki_export( 3, x509, "pem", tr("PEM selected"), F_PEM | F_MULTI, tr("Concatenated list of all selected items in one PEM text file")), +new pki_export( 3, x509, "pem", tr("PEM selected"), F_PEM | F_MULTI, tr("Concatenated list of all selected certificates in one PEM text file")), new pki_export( 2, x509, "pem", tr("PEM chain"), F_PEM | F_USUAL | F_CHAIN | F_SINGLE | F_CLIPBOARD, tr("Concatenated text format of the complete certificate chain in one PEM file")), -new pki_export( 4, x509, "ovpn","OpenVPN", F_PEM | F_OVPN | F_CHAIN | F_PRIVATE | F_SINGLE, tr("The complete certificate chain and the private key of the selected certificate with tags usable in OpenVPN configuration files")), +new pki_export( 4, x509, "ovpn", "OpenVPN", F_PEM | F_OVPN | F_CHAIN | F_PRIVATE | F_SINGLE, tr("The complete certificate chain and the private key of the selected certificate with tags usable in OpenVPN configuration files")), new pki_export( 6, x509, "pem", tr("PEM + key"), F_PEM | F_PRIVATE| F_SINGLE | F_CLIPBOARD, tr("Concatenation of the certificate and the unencrypted private key in one PEM file")), -new pki_export( 7, x509, "pem", "PEM + PKCS#8", F_PEM | F_PKCS8 | F_PRIVATE | F_CRYPT | F_CLIPBOARD, tr("Concatenation of the certificate and the encrypted private key in PKCS#8 format in one file")), -new pki_export(40, x509, "pem", "All unusable", F_PEM | F_UNUSABLE, tr("Concatenation of all expired or revoked certificates in one PEM file")), +new pki_export( 7, x509, "pem", "PEM + PKCS#8", F_PEM | F_PKCS8 | F_PRIVATE | F_CRYPT | F_SINGLE | F_CLIPBOARD, tr("Concatenation of the certificate and the encrypted private key in PKCS#8 format in one file")), +new pki_export(40, x509, "pem", tr("All unusable"), F_PEM | F_UNUSABLE, tr("Concatenation of all expired or revoked certificates in one PEM file")), new pki_export( 8, x509, "p7b", "PKCS #7", F_PKCS7 | F_USUAL | F_SINGLE, tr("PKCS#7 encoded single certificate")), new pki_export(10, x509, "p7b", "PKCS #7", F_PKCS7 | F_USUAL | F_MULTI, tr("All selected certificates encoded in one PKCS#7 file")), new pki_export(12, x509, "p7b", tr("PKCS #7 chain"), F_PKCS7 | F_USUAL | F_CHAIN | F_SINGLE, tr("PKCS#7 encoded complete certificate chain")), -new pki_export(41, x509, "p7b", "PKCS #7 Unusable", F_PKCS7 | F_UNUSABLE, tr("PKCS#7 encoded collection of all expired or revoked certificates")), +new pki_export(41, x509, "p7b", tr("PKCS #7 unusable"), F_PKCS7 | F_UNUSABLE, tr("PKCS#7 encoded collection of all expired or revoked certificates")), new pki_export(13, x509, "cer", "DER", F_DER | F_SINGLE, tr("Binary DER encoded certificate")), new pki_export(14, x509, "pfx", tr("PKCS #12 chain"),F_PKCS12 | F_USUAL | F_CHAIN | F_CRYPT | F_PRIVATE | F_SINGLE, tr("The complete certificate chain and the private key as encrypted PKCS#12 file")), new pki_export(15, x509, "pfx", tr("PKCS #12"), F_PKCS12 | F_USUAL | F_CRYPT | F_PRIVATE | F_SINGLE, tr("The certificate and the private key as encrypted PKCS#12 file")), diff --git a/test/export.cpp b/test/export.cpp index 6019b11e..2bf270fa 100644 --- a/test/export.cpp +++ b/test/export.cpp @@ -187,14 +187,14 @@ void test_main::exportFormat() idx = certs->index(certs->getByName("Root CA")); list << idx; QCOMPARE(certs->exportFlags(idx) , F_CHAIN | F_PRIVATE); - QCOMPARE(certs->exportFlags(list) , F_CHAIN | F_PRIVATE | F_MULTI); + QCOMPARE(certs->exportFlags(list) , F_CHAIN | F_PRIVATE); // Inter CA 1: All export options permitted // Together with "Root CA" in "list": No chain, private or single idx = certs->index(certs->getByName("Inter CA 1")); list << idx; QCOMPARE(certs->exportFlags(idx) , 0); - QCOMPARE(certs->exportFlags(list) , F_CHAIN | F_PRIVATE | F_SINGLE); + QCOMPARE(certs->exportFlags(list) , F_CHAIN | F_PRIVATE); // Endentity has no private key and id no CA idx = certs->index(certs->getByName("Endentity")); diff --git a/ui/ExportDialog.ui b/ui/ExportDialog.ui index 06673b93..f5405ed6 100644 --- a/ui/ExportDialog.ui +++ b/ui/ExportDialog.ui @@ -7,7 +7,7 @@ 0 0 589 - 343 + 366 @@ -22,6 +22,7 @@ Arial 14 + 50 false false false @@ -70,7 +71,7 @@ - + Name @@ -84,7 +85,7 @@ - + 0 @@ -114,6 +115,13 @@ + + + + Each Item in a separate file + + + diff --git a/widgets/ExportDialog.cpp b/widgets/ExportDialog.cpp index b06f7632..0a503142 100644 --- a/widgets/ExportDialog.cpp +++ b/widgets/ExportDialog.cpp @@ -23,9 +23,8 @@ ExportDialog::ExportDialog(QWidget *w, const QString &title, const QString &filt, const QModelIndexList &indexes, const QPixmap &img, QList types, const QString &help_ctx) - : QDialog(w ? w : mainwin) + : QDialog(w ? w : mainwin), filter(filt), savedFile(), alltypes(types) { - QList usual, normal; QString fname = "selected_items"; setupUi(this); setWindowTitle(XCA_TITLE); @@ -35,24 +34,37 @@ ExportDialog::ExportDialog(QWidget *w, const QString &title, description->setText(pki->getIntName()); fname = pki->getUnderlinedName(); } + separateFiles->hide(); + setupExportFormat(F_MULTI); + } else { + // Plural form not required for < 2 items + // Will only be called for 2 or more items + description->setText(tr("%n selected item(s)", "", indexes.size())); + setupExportFormat(F_SINGLE); } description->setReadOnly(true); image->setPixmap(img); label->setText(title); mainwin->helpdlg->register_ctxhelp_button(this, help_ctx); - QString fn = Settings["workingdir"] + - fname + "." + types[0]->extension; - filename->setText(nativeSeparator(fn)); + fname = Settings["workingdir"] + fname + "." + types[0]->extension; + filename->setText(nativeSeparator(fname)); + filter = tr("All files ( * )") + ";;" + filter; + filenameLabelOrig = filenameLabel->text(); +} - filter = tr("All files ( * )") + ";;" + filt; - - foreach(const pki_export *t, types) { +void ExportDialog::setupExportFormat(int disable_flag) +{ + QList usual, normal; + for (const pki_export *t : alltypes) { + if (t->flags & disable_flag) + continue; if (t->flags & F_USUAL) usual << t; else normal << t; } + exportFormat->clear(); foreach(const pki_export *t, usual + normal) { exportFormat->addItem(QString("%1 (*.%2)"). arg(t->desc).arg(t->extension), QVariant(t->id)); @@ -71,10 +83,15 @@ ExportDialog::~ExportDialog() void ExportDialog::on_fileBut_clicked() { - QString s = QFileDialog::getSaveFileName(this, QString(), - filename->text(), filter, NULL, - QFileDialog::DontConfirmOverwrite); - + QString s; + if (separateFiles->isChecked()) { + s = QFileDialog::getExistingDirectory(this, QString(), filename->text(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + } else { + s = QFileDialog::getSaveFileName(this, QString(), + filename->text(), filter, NULL, + QFileDialog::DontConfirmOverwrite); + } if (!s.isEmpty()) filename->setText(nativeSeparator(s)); } @@ -99,7 +116,8 @@ void ExportDialog::on_exportFormat_activated(int selected) bool ExportDialog::mayWriteFile(const QString &fname) { - if (QFile::exists(fname)) { + QFileInfo fi(fname); + if (fi.exists() && !fi.isDir()) { xcaWarningBox msg(NULL, tr("The file: '%1' already exists!").arg(fname)); msg.addButton(QMessageBox::Ok, tr("Overwrite")); @@ -145,3 +163,21 @@ void ExportDialog::on_exportFormat_highlighted(int index) infoBox->setText(x->help); pemComment->setEnabled(x->flags & F_PEM); } + +void ExportDialog::on_separateFiles_clicked(bool checked) +{ + if (checked) { + filenameLabel->setText(tr("Directory")); + QFileInfo fi(filename->text()); + savedFile = fi.fileName(); + filename->setText(nativeSeparator(fi.path())); + setupExportFormat(F_MULTI); + } else { + filenameLabel->setText(filenameLabelOrig); + if (!savedFile.isEmpty()) { + QString completefile = filename->text() + "/" + savedFile; + filename->setText(nativeSeparator(completefile)); + } + setupExportFormat(F_SINGLE); + } +} \ No newline at end of file diff --git a/widgets/ExportDialog.h b/widgets/ExportDialog.h index 1133d09d..55df272d 100644 --- a/widgets/ExportDialog.h +++ b/widgets/ExportDialog.h @@ -20,7 +20,8 @@ class ExportDialog: public QDialog, public Ui::ExportDialog Q_OBJECT protected: - QString filter{}; + QString filter{}, savedFile{}, filenameLabelOrig{}; + QList alltypes; public: ExportDialog(QWidget *w, const QString &title, const QString &filt, @@ -30,9 +31,11 @@ class ExportDialog: public QDialog, public Ui::ExportDialog ~ExportDialog(); static bool mayWriteFile(const QString &fname); const pki_export *export_type(int idx = -1) const; + void setupExportFormat(int disable_flag); public slots: void on_fileBut_clicked(); + void on_separateFiles_clicked(bool checked); void on_exportFormat_activated(int); void on_exportFormat_highlighted(int index); void accept(); diff --git a/widgets/XcaTreeView.cpp b/widgets/XcaTreeView.cpp index bd9148bd..20f46843 100644 --- a/widgets/XcaTreeView.cpp +++ b/widgets/XcaTreeView.cpp @@ -26,6 +26,8 @@ #include "ExportDialog.h" #include "ImportMulti.h" #include "lib/load_obj.h" +#include "lib/pass_info.h" +#include "lib/PwDialogCore.h" #include "ui_ItemProperties.h" @@ -480,24 +482,57 @@ void XcaTreeView::changeView() show(); } +void XcaTreeView::exportItems(const QModelIndexList &indexes, + const QString &filename, const pki_export *xport) +{ + XFile file(filename); + + if (xport->match_all(F_PRIVATE)) + file.open_key(); + else + file.open_write(); + + basemodel->exportItems(indexes, xport, file); +} + void XcaTreeView::exportItems(const QModelIndexList &indexes) { if (!basemodel || indexes.empty()) return; ExportDialog *dlg = exportDialog(indexes); - if (dlg && dlg->exec()) { try { + QString fname = dlg->filename->text(); const pki_export *xport = dlg->export_type(); - XFile file(dlg->filename->text()); - if (xport->match_all(F_PRIVATE)) - file.open_key(); - else - file.open_write(); + if (dlg->separateFiles->isChecked()) { + Passwd pass; + if (xport->match_all(F_CRYPT)) { + // Plural form not required for < 2 items + // Will only be called for 2 or more items + pass_info p(tr("Export Password"), + tr("Please enter the password to encrypt all %n " + "exported private key(s) in:\n%1", "", + indexes.size()).arg(fname), + this); - basemodel->exportItems(indexes, xport, file); + // Ask for an encryption password once + if (PwDialogCore::execute(&p, &pass, true) != 1) + return; + } + for (QModelIndex idx : indexes) { + pki_base *pki = db_base::fromIndex(idx); + // Will be cleared by PwDialogCore + PwDialogCore::cmdline_passwd = pass; + QString fn = pki->get_dump_filename(fname, + QString(".%1").arg(xport->extension)); + exportItems(QModelIndexList() << idx, fn, xport); + } + PwDialogCore::cmdline_passwd.cleanse(); + } else { + exportItems(indexes, fname, xport); + } } catch (errorEx &err) { XCA_ERROR(err); } diff --git a/widgets/XcaTreeView.h b/widgets/XcaTreeView.h index 2ec4a367..0fd530ef 100644 --- a/widgets/XcaTreeView.h +++ b/widgets/XcaTreeView.h @@ -68,6 +68,8 @@ class XcaTreeView: public QTreeView virtual void exportItems(const QModelIndexList &indexes); virtual void load_default(load_base *load); virtual ExportDialog *exportDialog(const QModelIndexList &indexes); + virtual void exportItems(const QModelIndexList &indexes, + const QString &filename, const pki_export *xport); public slots: void changeView();