diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 6825ce6c0..fd9872cee 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -47,6 +47,11 @@ if(Poppler_Qt5_FOUND) TEST_NAME "formattest" LINK_LIBRARIES Qt5::Widgets Qt5::Test okularcore ) + + ecm_add_test(signunsignedfieldtest + TEST_NAME "signunsignedfieldtest" + LINK_LIBRARIES Qt5::Widgets Qt5::Test okularcore + ) endif() ecm_add_test(documenttest.cpp diff --git a/autotests/data/fake_okular_certstore/cert9.db b/autotests/data/fake_okular_certstore/cert9.db new file mode 100644 index 000000000..04959dcbb Binary files /dev/null and b/autotests/data/fake_okular_certstore/cert9.db differ diff --git a/autotests/data/fake_okular_certstore/key4.db b/autotests/data/fake_okular_certstore/key4.db new file mode 100644 index 000000000..4d990e8dd Binary files /dev/null and b/autotests/data/fake_okular_certstore/key4.db differ diff --git a/autotests/data/fake_okular_certstore/pkcs11.txt b/autotests/data/fake_okular_certstore/pkcs11.txt new file mode 100644 index 000000000..17374ec6b --- /dev/null +++ b/autotests/data/fake_okular_certstore/pkcs11.txt @@ -0,0 +1,5 @@ +library= +name=NSS Internal PKCS #11 Module +parameters=configdir='testest' certPrefix='' keyPrefix='' secmod='secmod.db' flags= updatedir='' updateCertPrefix='' updateKeyPrefix='' updateid='' updateTokenDescription='' +NSS=Flags=internal,critical trustOrder=75 cipherOrder=100 slotParams=(1={slotFlags=[ECC,RSA,DSA,DH,RC2,RC4,DES,RANDOM,SHA1,MD5,MD2,SSL,TLS,AES,Camellia,SEED,SHA256,SHA512] askpw=any timeout=30}) + diff --git a/autotests/data/hello_with_dummy_signature.pdf b/autotests/data/hello_with_dummy_signature.pdf new file mode 100644 index 000000000..c4d222e4d Binary files /dev/null and b/autotests/data/hello_with_dummy_signature.pdf differ diff --git a/autotests/signunsignedfieldtest.cpp b/autotests/signunsignedfieldtest.cpp new file mode 100644 index 000000000..7e3de19bf --- /dev/null +++ b/autotests/signunsignedfieldtest.cpp @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2022 Albert Astals Cid + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/document.h" +#include "../core/form.h" +#include "../core/page.h" +#include "../settings_core.h" + +class EnterPasswordDialogHelper : public QObject +{ + Q_OBJECT + +public: + EnterPasswordDialogHelper() + { + QTimer::singleShot(0, this, &EnterPasswordDialogHelper::enterPassword); + } + + void enterPassword() + { + QWidget *dialog = qApp->activeModalWidget(); + if (!dialog) { + QTimer::singleShot(0, this, &EnterPasswordDialogHelper::enterPassword); + return; + } + QLineEdit *lineEdit = dialog->findChild(); + lineEdit->setText(QStringLiteral("fakeokular")); + + QDialogButtonBox *buttonBox = dialog->findChild(); + buttonBox->button(QDialogButtonBox::Ok)->click(); + } +}; + +class SignUnsignedFieldTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void init(); + void cleanup(); + void testSignUnsignedField(); + +private: + Okular::Document *m_document; +}; + +void SignUnsignedFieldTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + Okular::SettingsCore::instance(QStringLiteral("signunsignedfieldtest")); + + KConfig cfg(QStringLiteral("okular-generator-popplerrc")); + KConfigGroup g = cfg.group(QStringLiteral("Signatures")); + g.writeEntry(QStringLiteral("UseDefaultCertDB"), false); + g.writeEntry(QStringLiteral("DBCertificatePath"), "file://" KDESRCDIR "data/fake_okular_certstore"); + + m_document = new Okular::Document(nullptr); +} + +void SignUnsignedFieldTest::init() +{ + const QString testFile = QStringLiteral(KDESRCDIR "data/hello_with_dummy_signature.pdf"); + QMimeDatabase db; + const QMimeType mime = db.mimeTypeForFile(testFile); + QCOMPARE(m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess); +} + +void SignUnsignedFieldTest::cleanup() +{ + m_document->closeDocument(); +} + +void SignUnsignedFieldTest::testSignUnsignedField() +{ + const QLinkedList forms = m_document->page(0)->formFields(); + QCOMPARE(forms.count(), 1); + Okular::FormFieldSignature *ffs = dynamic_cast(forms.first()); + + // This is a hacky way of doing ifdef HAVE_POPPLER_22_02 + if (m_document->metaData(QStringLiteral("CanSignDocumentWithPassword")).toString() == QLatin1String("yes")) { + QCOMPARE(ffs->signatureType(), Okular::FormFieldSignature::UnsignedSignature); + + const Okular::CertificateStore *certStore = m_document->certificateStore(); + bool userCancelled, nonDateValidCerts; + { + EnterPasswordDialogHelper helper; + const QList &certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts); + QCOMPARE(certs.count(), 1); + } + + Okular::NewSignatureData data; + data.setCertNickname(QStringLiteral("fake-okular")); + QTemporaryFile f; + f.open(); + QVERIFY(ffs->sign(data, f.fileName())); + + m_document->closeDocument(); + QMimeDatabase db; + const QMimeType mime = db.mimeTypeForFile(f.fileName()); + QCOMPARE(m_document->openDocument(f.fileName(), QUrl(), mime), Okular::Document::OpenSuccess); + + const QLinkedList newForms = m_document->page(0)->formFields(); + QCOMPARE(newForms.count(), 1); + ffs = dynamic_cast(newForms.first()); + QCOMPARE(ffs->signatureType(), Okular::FormFieldSignature::AdbePkcs7detached); + QCOMPARE(ffs->signatureInfo().signerName(), "FakeOkular"); + } +} + +QTEST_MAIN(SignUnsignedFieldTest) +#include "signunsignedfieldtest.moc" diff --git a/core/form.h b/core/form.h index d977cc38d..b841e6cfa 100644 --- a/core/form.h +++ b/core/form.h @@ -9,6 +9,7 @@ #include "annotations.h" #include "area.h" +#include "document.h" #include "okularcore_export.h" #include "signatureutils.h" @@ -456,7 +457,13 @@ public: /** * The types of signature. */ - enum SignatureType { AdbePkcs7sha1, AdbePkcs7detached, EtsiCAdESdetached, UnknownType }; + enum SignatureType { + AdbePkcs7sha1, + AdbePkcs7detached, + EtsiCAdESdetached, + UnknownType, + UnsignedSignature ///< The signature field has not been signed yet. @since 22.04 + }; ~FormFieldSignature() override; @@ -470,6 +477,13 @@ public: */ virtual const SignatureInfo &signatureInfo() const = 0; + /** + Signs a field of UnsignedSignature type. + + @since 22.04 + */ + virtual bool sign(const NewSignatureData &data, const QString &newPath) const = 0; + protected: FormFieldSignature(); diff --git a/generators/poppler/CMakeLists.txt b/generators/poppler/CMakeLists.txt index 82fb63aab..23efdd06b 100644 --- a/generators/poppler/CMakeLists.txt +++ b/generators/poppler/CMakeLists.txt @@ -60,6 +60,7 @@ check_cxx_source_compiles(" #include int main() { + auto us = Poppler::FormFieldSignature::UnsignedSignature; Poppler::PDFConverter::NewSignatureData pData; pData.setDocumentOwnerPassword(QByteArray()); } diff --git a/generators/poppler/formfields.cpp b/generators/poppler/formfields.cpp index da9de82bc..6b9d20a8e 100644 --- a/generators/poppler/formfields.cpp +++ b/generators/poppler/formfields.cpp @@ -10,6 +10,7 @@ #include "core/action.h" +#include "generator_pdf.h" #include "pdfsettings.h" #include "pdfsignatureutils.h" @@ -453,6 +454,10 @@ PopplerFormFieldSignature::SignatureType PopplerFormFieldSignature::signatureTyp return Okular::FormFieldSignature::AdbePkcs7detached; case Poppler::FormFieldSignature::EtsiCAdESdetached: return Okular::FormFieldSignature::EtsiCAdESdetached; +#ifdef HAVE_POPPLER_22_02 + case Poppler::FormFieldSignature::UnsignedSignature: + return Okular::FormFieldSignature::UnsignedSignature; +#endif default: return Okular::FormFieldSignature::UnknownType; } @@ -462,3 +467,16 @@ const Okular::SignatureInfo &PopplerFormFieldSignature::signatureInfo() const { return *m_info; } + +bool PopplerFormFieldSignature::sign(const Okular::NewSignatureData &oData, const QString &newPath) const +{ +#ifdef HAVE_POPPLER_22_02 + Poppler::PDFConverter::NewSignatureData pData; + PDFGenerator::okularToPoppler(oData, &pData); + return m_field->sign(newPath, pData) == Poppler::FormFieldSignature::SigningSuccess; +#else + Q_UNUSED(oData) + Q_UNUSED(newPath) + return false; +#endif +} diff --git a/generators/poppler/formfields.h b/generators/poppler/formfields.h index ec0cfaaa2..c9cee593b 100644 --- a/generators/poppler/formfields.h +++ b/generators/poppler/formfields.h @@ -139,6 +139,7 @@ public: // inherited from Okular::FormFieldSignature SignatureType signatureType() const override; const Okular::SignatureInfo &signatureInfo() const override; + bool sign(const Okular::NewSignatureData &oData, const QString &newPath) const override; private: std::unique_ptr m_field; diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index eac223045..7ba5a0c9e 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1312,6 +1312,28 @@ QByteArray PDFGenerator::requestFontData(const Okular::FontInfo &font) return pdfdoc->fontData(fi); } +#ifdef HAVE_POPPLER_SIGNING +void PDFGenerator::okularToPoppler(const Okular::NewSignatureData &oData, Poppler::PDFConverter::NewSignatureData *pData) +{ + pData->setCertNickname(oData.certNickname()); + pData->setPassword(oData.password()); + pData->setPage(oData.page()); + const QString datetime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss t")); + pData->setSignatureText(i18n("Signed by: %1\n\nDate: %2", oData.certSubjectCommonName(), datetime)); +#ifdef HAVE_POPPLER_FANCY_SIGNATURE + pData->setSignatureLeftText(oData.certSubjectCommonName()); +#endif + const Okular::NormalizedRect bRect = oData.boundingRectangle(); + pData->setBoundingRectangle({bRect.left, bRect.top, bRect.width(), bRect.height()}); + pData->setFontColor(Qt::black); + pData->setBorderColor(Qt::black); +#ifdef HAVE_POPPLER_22_02 + pData->setDocumentOwnerPassword(oData.documentPassword().toLatin1()); + pData->setDocumentUserPassword(oData.documentPassword().toLatin1()); +#endif +} +#endif + #define DUMMY_QPRINTER_COPY Okular::Document::PrintError PDFGenerator::print(QPrinter &printer) { @@ -1954,22 +1976,7 @@ bool PDFGenerator::sign(const Okular::NewSignatureData &oData, const QString &rF converter->setPDFOptions(converter->pdfOptions() | Poppler::PDFConverter::WithChanges); Poppler::PDFConverter::NewSignatureData pData; - pData.setCertNickname(oData.certNickname()); - pData.setPassword(oData.password()); - pData.setPage(oData.page()); - const QString datetime = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss t")); - pData.setSignatureText(i18n("Signed by: %1\n\nDate: %2", oData.certSubjectCommonName(), datetime)); -#ifdef HAVE_POPPLER_FANCY_SIGNATURE - pData.setSignatureLeftText(oData.certSubjectCommonName()); -#endif - const Okular::NormalizedRect bRect = oData.boundingRectangle(); - pData.setBoundingRectangle({bRect.left, bRect.top, bRect.width(), bRect.height()}); - pData.setFontColor(Qt::black); - pData.setBorderColor(Qt::black); -#ifdef HAVE_POPPLER_22_02 - pData.setDocumentOwnerPassword(oData.documentPassword().toLatin1()); - pData.setDocumentUserPassword(oData.documentPassword().toLatin1()); -#endif + okularToPoppler(oData, &pData); if (!converter->sign(pData)) return false; diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index 1b7bcc1a6..d58b4a333 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -108,6 +108,10 @@ public: QByteArray requestFontData(const Okular::FontInfo &font) override; +#ifdef HAVE_POPPLER_SIGNING + static void okularToPoppler(const Okular::NewSignatureData &oData, Poppler::PDFConverter::NewSignatureData *pData); +#endif + protected: SwapBackingFileResult swapBackingFile(QString const &newFileName, QVector &newPagesVector) override; bool doCloseDocument() override; diff --git a/part/formwidgets.cpp b/part/formwidgets.cpp index c29400691..4a911f51b 100644 --- a/part/formwidgets.cpp +++ b/part/formwidgets.cpp @@ -9,8 +9,10 @@ */ #include "formwidgets.h" +#include "pageview.h" #include "pageviewutils.h" #include "revisionviewer.h" +#include "signatureguiutils.h" #include "signaturepropertiesdialog.h" #include @@ -222,7 +224,7 @@ void FormWidgetsController::slotFormButtonsChangedByUndoRedo(int pageNumber, con emit changed(pageNumber); } -FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget *parent) +FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, PageView *pageView) { FormWidgetIface *widget = nullptr; @@ -231,13 +233,13 @@ FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget Okular::FormFieldButton *ffb = static_cast(ff); switch (ffb->buttonType()) { case Okular::FormFieldButton::Push: - widget = new PushButtonEdit(ffb, parent); + widget = new PushButtonEdit(ffb, pageView); break; case Okular::FormFieldButton::CheckBox: - widget = new CheckBoxEdit(ffb, parent); + widget = new CheckBoxEdit(ffb, pageView); break; case Okular::FormFieldButton::Radio: - widget = new RadioButtonEdit(ffb, parent); + widget = new RadioButtonEdit(ffb, pageView); break; default:; } @@ -247,13 +249,13 @@ FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget Okular::FormFieldText *fft = static_cast(ff); switch (fft->textType()) { case Okular::FormFieldText::Multiline: - widget = new TextAreaEdit(fft, parent); + widget = new TextAreaEdit(fft, pageView); break; case Okular::FormFieldText::Normal: - widget = new FormLineEdit(fft, parent); + widget = new FormLineEdit(fft, pageView); break; case Okular::FormFieldText::FileSelect: - widget = new FileEdit(fft, parent); + widget = new FileEdit(fft, pageView); break; } break; @@ -262,10 +264,10 @@ FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget Okular::FormFieldChoice *ffc = static_cast(ff); switch (ffc->choiceType()) { case Okular::FormFieldChoice::ListBox: - widget = new ListEdit(ffc, parent); + widget = new ListEdit(ffc, pageView); break; case Okular::FormFieldChoice::ComboBox: - widget = new ComboEdit(ffc, parent); + widget = new ComboEdit(ffc, pageView); break; } break; @@ -273,7 +275,7 @@ FormWidgetIface *FormWidgetFactory::createWidget(Okular::FormField *ff, QWidget case Okular::FormField::FormSignature: { Okular::FormFieldSignature *ffs = static_cast(ff); if (ffs->isVisible() && ffs->signatureType() != Okular::FormFieldSignature::UnknownType) - widget = new SignatureEdit(ffs, parent); + widget = new SignatureEdit(ffs, pageView); break; } default:; @@ -363,8 +365,8 @@ void FormWidgetIface::slotRefresh(Okular::FormField *form) m_widget->setEnabled(!form->isReadOnly()); } -PushButtonEdit::PushButtonEdit(Okular::FormFieldButton *button, QWidget *parent) - : QPushButton(parent) +PushButtonEdit::PushButtonEdit(Okular::FormFieldButton *button, PageView *pageView) + : QPushButton(pageView->viewport()) , FormWidgetIface(this, button) { setText(button->caption()); @@ -377,8 +379,8 @@ PushButtonEdit::PushButtonEdit(Okular::FormFieldButton *button, QWidget *parent) setCursor(Qt::ArrowCursor); } -CheckBoxEdit::CheckBoxEdit(Okular::FormFieldButton *button, QWidget *parent) - : QCheckBox(parent) +CheckBoxEdit::CheckBoxEdit(Okular::FormFieldButton *button, PageView *pageView) + : QCheckBox(pageView->viewport()) , FormWidgetIface(this, button) { setText(button->caption()); @@ -418,8 +420,8 @@ void CheckBoxEdit::slotRefresh(Okular::FormField *form) } } -RadioButtonEdit::RadioButtonEdit(Okular::FormFieldButton *button, QWidget *parent) - : QRadioButton(parent) +RadioButtonEdit::RadioButtonEdit(Okular::FormFieldButton *button, PageView *pageView) + : QRadioButton(pageView->viewport()) , FormWidgetIface(this, button) { setText(button->caption()); @@ -436,8 +438,8 @@ void RadioButtonEdit::setFormWidgetsController(FormWidgetsController *controller setChecked(form->state()); } -FormLineEdit::FormLineEdit(Okular::FormFieldText *text, QWidget *parent) - : QLineEdit(parent) +FormLineEdit::FormLineEdit(Okular::FormFieldText *text, PageView *pageView) + : QLineEdit(pageView->viewport()) , FormWidgetIface(this, text) { int maxlen = text->maximumLength(); @@ -594,8 +596,8 @@ void FormLineEdit::slotRefresh(Okular::FormField *form) setText(text->text()); } -TextAreaEdit::TextAreaEdit(Okular::FormFieldText *text, QWidget *parent) - : KTextEdit(parent) +TextAreaEdit::TextAreaEdit(Okular::FormFieldText *text, PageView *pageView) + : KTextEdit(pageView->viewport()) , FormWidgetIface(this, text) { setAcceptRichText(text->isRichText()); @@ -730,8 +732,8 @@ void TextAreaEdit::slotRefresh(Okular::FormField *form) setPlainText(text->text()); } -FileEdit::FileEdit(Okular::FormFieldText *text, QWidget *parent) - : KUrlRequester(parent) +FileEdit::FileEdit(Okular::FormFieldText *text, PageView *pageView) + : KUrlRequester(pageView->viewport()) , FormWidgetIface(this, text) { setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); @@ -839,8 +841,8 @@ void FileEdit::slotHandleFileChangedByUndoRedo(int pageNumber, Okular::FormField setFocus(); } -ListEdit::ListEdit(Okular::FormFieldChoice *choice, QWidget *parent) - : QListWidget(parent) +ListEdit::ListEdit(Okular::FormFieldChoice *choice, PageView *pageView) + : QListWidget(pageView->viewport()) , FormWidgetIface(this, choice) { addItems(choice->choices()); @@ -900,8 +902,8 @@ void ListEdit::slotHandleFormListChangedByUndoRedo(int pageNumber, Okular::FormF setFocus(); } -ComboEdit::ComboEdit(Okular::FormFieldChoice *choice, QWidget *parent) - : QComboBox(parent) +ComboEdit::ComboEdit(Okular::FormFieldChoice *choice, PageView *pageView) + : QComboBox(pageView->viewport()) , FormWidgetIface(this, choice) { addItems(choice->choices()); @@ -1035,15 +1037,20 @@ bool ComboEdit::event(QEvent *e) return QComboBox::event(e); } -SignatureEdit::SignatureEdit(Okular::FormFieldSignature *signature, QWidget *parent) - : QAbstractButton(parent) +SignatureEdit::SignatureEdit(Okular::FormFieldSignature *signature, PageView *pageView) + : QAbstractButton(pageView->viewport()) , FormWidgetIface(this, signature) , m_widgetPressed(false) , m_dummyMode(false) , m_wasVisible(false) { setCursor(Qt::PointingHandCursor); - connect(this, &SignatureEdit::clicked, this, &SignatureEdit::slotViewProperties); + if (signature->signatureType() == Okular::FormFieldSignature::UnsignedSignature) { + setToolTip(i18n("Unsigned Signature Field (Click to Sign)")); + connect(this, &SignatureEdit::clicked, this, &SignatureEdit::signUnsignedSignature); + } else { + connect(this, &SignatureEdit::clicked, this, &SignatureEdit::slotViewProperties); + } } void SignatureEdit::setDummyMode(bool set) @@ -1102,9 +1109,16 @@ bool SignatureEdit::event(QEvent *e) void SignatureEdit::contextMenuEvent(QContextMenuEvent *event) { QMenu *menu = new QMenu(this); - QAction *signatureProperties = new QAction(i18n("Signature Properties"), menu); - connect(signatureProperties, &QAction::triggered, this, &SignatureEdit::slotViewProperties); - menu->addAction(signatureProperties); + Okular::FormFieldSignature *formSignature = static_cast(formField()); + if (formSignature->signatureType() == Okular::FormFieldSignature::UnsignedSignature) { + QAction *signAction = new QAction(i18n("&Sign..."), menu); + connect(signAction, &QAction::triggered, this, &SignatureEdit::signUnsignedSignature); + menu->addAction(signAction); + } else { + QAction *signatureProperties = new QAction(i18n("Signature Properties"), menu); + connect(signatureProperties, &QAction::triggered, this, &SignatureEdit::slotViewProperties); + menu->addAction(signatureProperties); + } menu->exec(event->globalPos()); delete menu; } @@ -1139,6 +1153,16 @@ void SignatureEdit::slotViewProperties() propDlg.exec(); } +void SignatureEdit::signUnsignedSignature() +{ + if (m_dummyMode) + return; + + Okular::FormFieldSignature *formSignature = static_cast(formField()); + PageView *pageView = static_cast(parent()->parent()); + SignatureGuiUtils::signUnsignedSignature(formSignature, pageView, pageView->document()); +} + // Code for additional action handling. // Challenge: Change preprocessor magic to C++ magic! // diff --git a/part/formwidgets.h b/part/formwidgets.h index 89ccb1b57..9e54581a6 100644 --- a/part/formwidgets.h +++ b/part/formwidgets.h @@ -26,6 +26,7 @@ class ComboEdit; class QMenu; class QButtonGroup; class FormWidgetIface; +class PageView; class PageViewItem; class RadioButtonEdit; class QEvent; @@ -121,7 +122,7 @@ private: class FormWidgetFactory { public: - static FormWidgetIface *createWidget(Okular::FormField *ff, QWidget *parent = nullptr); + static FormWidgetIface *createWidget(Okular::FormField *ff, PageView *pageView); }; class FormWidgetIface @@ -171,7 +172,7 @@ class PushButtonEdit : public QPushButton, public FormWidgetIface Q_OBJECT public: - explicit PushButtonEdit(Okular::FormFieldButton *button, QWidget *parent = nullptr); + explicit PushButtonEdit(Okular::FormFieldButton *button, PageView *pageView); DECLARE_ADDITIONAL_ACTIONS }; @@ -181,7 +182,7 @@ class CheckBoxEdit : public QCheckBox, public FormWidgetIface Q_OBJECT public: - explicit CheckBoxEdit(Okular::FormFieldButton *button, QWidget *parent = nullptr); + explicit CheckBoxEdit(Okular::FormFieldButton *button, PageView *pageView); // reimplemented from FormWidgetIface void setFormWidgetsController(FormWidgetsController *controller) override; @@ -198,7 +199,7 @@ class RadioButtonEdit : public QRadioButton, public FormWidgetIface Q_OBJECT public: - explicit RadioButtonEdit(Okular::FormFieldButton *button, QWidget *parent = nullptr); + explicit RadioButtonEdit(Okular::FormFieldButton *button, PageView *pageView); // reimplemented from FormWidgetIface void setFormWidgetsController(FormWidgetsController *controller) override; @@ -210,7 +211,7 @@ class FormLineEdit : public QLineEdit, public FormWidgetIface Q_OBJECT public: - explicit FormLineEdit(Okular::FormFieldText *text, QWidget *parent = nullptr); + explicit FormLineEdit(Okular::FormFieldText *text, PageView *pageView); void setFormWidgetsController(FormWidgetsController *controller) override; bool event(QEvent *e) override; void contextMenuEvent(QContextMenuEvent *event) override; @@ -235,7 +236,7 @@ class TextAreaEdit : public KTextEdit, public FormWidgetIface Q_OBJECT public: - explicit TextAreaEdit(Okular::FormFieldText *text, QWidget *parent = nullptr); + explicit TextAreaEdit(Okular::FormFieldText *text, PageView *pageView); ~TextAreaEdit() override; void setFormWidgetsController(FormWidgetsController *controller) override; bool event(QEvent *e) override; @@ -262,7 +263,7 @@ class FileEdit : public KUrlRequester, public FormWidgetIface Q_OBJECT public: - explicit FileEdit(Okular::FormFieldText *text, QWidget *parent = nullptr); + explicit FileEdit(Okular::FormFieldText *text, PageView *pageView); void setFormWidgetsController(FormWidgetsController *controller) override; protected: @@ -283,7 +284,7 @@ class ListEdit : public QListWidget, public FormWidgetIface Q_OBJECT public: - explicit ListEdit(Okular::FormFieldChoice *choice, QWidget *parent = nullptr); + explicit ListEdit(Okular::FormFieldChoice *choice, PageView *pageView); void setFormWidgetsController(FormWidgetsController *controller) override; private Q_SLOTS: @@ -297,7 +298,7 @@ class ComboEdit : public QComboBox, public FormWidgetIface Q_OBJECT public: - explicit ComboEdit(Okular::FormFieldChoice *choice, QWidget *parent = nullptr); + explicit ComboEdit(Okular::FormFieldChoice *choice, PageView *pageView); void setFormWidgetsController(FormWidgetsController *controller) override; bool event(QEvent *e) override; void contextMenuEvent(QContextMenuEvent *event) override; @@ -317,7 +318,7 @@ class SignatureEdit : public QAbstractButton, public FormWidgetIface Q_OBJECT public: - explicit SignatureEdit(Okular::FormFieldSignature *signature, QWidget *parent = nullptr); + explicit SignatureEdit(Okular::FormFieldSignature *signature, PageView *pageView); // This will be called when an item in signature panel is clicked. Calling it changes the // widget state. If this widget was visible prior to calling this then background @@ -332,6 +333,7 @@ protected: private Q_SLOTS: void slotViewProperties(); + void signUnsignedSignature(); private: bool m_widgetPressed; diff --git a/part/pageview.cpp b/part/pageview.cpp index ac73069c3..c677711e1 100644 --- a/part/pageview.cpp +++ b/part/pageview.cpp @@ -1252,7 +1252,7 @@ void PageView::notifySetup(const QVector &pageSet, int setupFlag #endif const QLinkedList pageFields = page->formFields(); for (Okular::FormField *ff : pageFields) { - FormWidgetIface *w = FormWidgetFactory::createWidget(ff, viewport()); + FormWidgetIface *w = FormWidgetFactory::createWidget(ff, this); if (w) { w->setPageItem(item); w->setFormWidgetsController(d->formWidgetsController()); @@ -4894,6 +4894,11 @@ void PageView::showNoSigningCertificatesDialog(bool nonDateValidCerts) } } +Okular::Document *PageView::document() const +{ + return d->document; +} + void PageView::slotSignature() { if (!d->document->isHistoryClean()) { diff --git a/part/pageview.h b/part/pageview.h index 0297ea581..2a942b721 100644 --- a/part/pageview.h +++ b/part/pageview.h @@ -117,6 +117,8 @@ public: void showNoSigningCertificatesDialog(bool nonDateValidCerts); + Okular::Document *document() const; + public Q_SLOTS: void copyTextSelection() const; void selectAll(); diff --git a/part/pageviewannotator.cpp b/part/pageviewannotator.cpp index 4a076e3bf..e15d63d54 100644 --- a/part/pageviewannotator.cpp +++ b/part/pageviewannotator.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -45,6 +44,7 @@ #include "guiutils.h" #include "pageview.h" #include "settings.h" +#include "signatureguiutils.h" /** @short PickPointEngine */ class PickPointEngine : public AnnotatorEngine @@ -354,75 +354,19 @@ public: } } - const Okular::CertificateStore *certStore = m_document->certificateStore(); - bool userCancelled, nonDateValidCerts; - const QList &certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts); - if (userCancelled) { + const std::unique_ptr cert = SignatureGuiUtils::getCertificateAndPasswordForSigning(m_pageView, m_document, &passToUse, &documentPassword); + if (!cert) { m_aborted = true; - return {}; - } - - if (certs.isEmpty()) { - m_creationCompleted = false; - clicked = false; - m_pageView->showNoSigningCertificatesDialog(nonDateValidCerts); - return {}; - } - - QStringList items; - QHash nickToCert; - for (auto cert : certs) { - items.append(cert->nickName()); - nickToCert[cert->nickName()] = cert; - } - - bool resok = false; - certNicknameToUse = QInputDialog::getItem(m_pageView, i18n("Select certificate to sign with"), i18n("Certificates:"), items, 0, false, &resok); - - if (resok) { - // I could not find any case in which i need to enter a password to use the certificate, seems that once you unlcok the firefox/NSS database - // you don't need a password anymore, but still there's code to do that in NSS so we have code to ask for it if needed. What we do is - // ask if the empty password is fine, if it is we don't ask the user anything, if it's not, we ask for a password - Okular::CertificateInfo *cert = nickToCert.value(certNicknameToUse); - bool passok = cert->checkPassword(QString()); - while (!passok) { - const QString title = i18n("Enter password (if any) to unlock certificate: %1", certNicknameToUse); - bool ok; - passToUse = QInputDialog::getText(m_pageView, i18n("Enter certificate password"), title, QLineEdit::Password, QString(), &ok); - if (ok) { - passok = cert->checkPassword(passToUse); - } else { - passok = false; - break; - } - } - - if (passok) { - certCommonName = cert->subjectInfo(Okular::CertificateInfo::CommonName); - } else { - certNicknameToUse.clear(); - m_aborted = true; - } + passToUse.clear(); + documentPassword.clear(); } else { - // The Cancel button has been clicked in the certificate dialog. - certNicknameToUse.clear(); - m_aborted = true; - } - - if (m_document->metaData(QStringLiteral("DocumentHasPassword")).toString() == QLatin1String("yes")) { - bool ok; - documentPassword = QInputDialog::getText(m_pageView, i18n("Enter document password"), i18n("Enter document password"), QLineEdit::Password, QString(), &ok); - if (!ok) { - passToUse.clear(); - m_aborted = true; - } + certNicknameToUse = cert->nickName(); + certCommonName = cert->subjectInfo(Okular::CertificateInfo::CommonName); } m_creationCompleted = false; clicked = false; - qDeleteAll(certs); - return {}; } @@ -1041,18 +985,7 @@ QRect PageViewAnnotator::performRouteMouseOrTabletEvent(const AnnotatorEngine::E if (signatureMode()) { auto signEngine = static_cast(m_engine); if (signEngine->isAccepted()) { - QMimeDatabase db; - const QString typeName = m_document->documentInfo().get(Okular::DocumentInfo::MimeType); - const QMimeType mimeType = db.mimeTypeForName(typeName); - const QString mimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", mimeType.comment(), mimeType.globPatterns().join(QLatin1Char(' '))); - - const QUrl currentFileUrl = m_document->currentDocument(); - const QFileInfo currentFileInfo(currentFileUrl.fileName()); - const QString localFilePathIfAny = currentFileUrl.isLocalFile() ? QFileInfo(currentFileUrl.path()).canonicalPath() + QLatin1Char('/') : QString(); - const QString newFileName = - localFilePathIfAny + i18nc("Used when suggesting a new name for a digitally signed file. %1 is the old file name and %2 it's extension", "%1_signed.%2", currentFileInfo.baseName(), currentFileInfo.completeSuffix()); - - const QString newFilePath = QFileDialog::getSaveFileName(m_pageView, i18n("Save Signed File As"), newFileName, mimeTypeFilter); + const QString newFilePath = SignatureGuiUtils::getFileNameForNewSignedFile(m_pageView, m_document); if (!newFilePath.isEmpty()) { const bool success = static_cast(m_engine)->sign(newFilePath); diff --git a/part/part.cpp b/part/part.cpp index 2c53feaee..57882ef04 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1589,14 +1589,22 @@ bool Part::openFile() } else { const QVector signatureFormFields = SignatureGuiUtils::getSignatureFormFields(m_document); bool allSignaturesValid = true; + bool anySignatureUnsigned = false; for (const Okular::FormFieldSignature *signature : signatureFormFields) { - const Okular::SignatureInfo &info = signature->signatureInfo(); - if (info.signatureStatus() != SignatureInfo::SignatureValid) { - allSignaturesValid = false; + if (signature->signatureType() == Okular::FormFieldSignature::UnsignedSignature) { + anySignatureUnsigned = true; + } else { + const Okular::SignatureInfo &info = signature->signatureInfo(); + if (info.signatureStatus() != SignatureInfo::SignatureValid) { + allSignaturesValid = false; + } } } - if (allSignaturesValid) { + if (anySignatureUnsigned) { + m_signatureMessage->setMessageType(KMessageWidget::Information); + m_signatureMessage->setText(i18n("This document has unsigned signature fields.")); + } else if (allSignaturesValid) { if (signatureFormFields.last()->signatureInfo().signsTotalDocument()) { m_signatureMessage->setMessageType(KMessageWidget::Information); m_signatureMessage->setText(i18n("This document is digitally signed.")); diff --git a/part/signatureguiutils.cpp b/part/signatureguiutils.cpp index e497038ac..4206e8390 100644 --- a/part/signatureguiutils.cpp +++ b/part/signatureguiutils.cpp @@ -6,11 +6,18 @@ #include "signatureguiutils.h" +#include +#include +#include +#include + #include +#include #include "core/document.h" #include "core/form.h" #include "core/page.h" +#include "pageview.h" namespace SignatureGuiUtils { @@ -145,4 +152,106 @@ QString getReadableKeyUsageNewLineSeparated(Okular::CertificateInfo::KeyUsageExt return getReadableKeyUsage(kuExtensions, QStringLiteral("\n")); } +std::unique_ptr getCertificateAndPasswordForSigning(PageView *pageView, Okular::Document *doc, QString *password, QString *documentPassword) +{ + const Okular::CertificateStore *certStore = doc->certificateStore(); + bool userCancelled, nonDateValidCerts; + QList certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts); + if (userCancelled) { + return nullptr; + } + + if (certs.isEmpty()) { + pageView->showNoSigningCertificatesDialog(nonDateValidCerts); + return nullptr; + } + + QStringList items; + QHash nickToCert; + for (auto cert : qAsConst(certs)) { + items.append(cert->nickName()); + nickToCert[cert->nickName()] = cert; + } + + bool resok = false; + const QString certNicknameToUse = QInputDialog::getItem(pageView, i18n("Select certificate to sign with"), i18n("Certificates:"), items, 0, false, &resok); + + if (!resok) { + qDeleteAll(certs); + return nullptr; + } + + // I could not find any case in which i need to enter a password to use the certificate, seems that once you unlcok the firefox/NSS database + // you don't need a password anymore, but still there's code to do that in NSS so we have code to ask for it if needed. What we do is + // ask if the empty password is fine, if it is we don't ask the user anything, if it's not, we ask for a password + Okular::CertificateInfo *cert = nickToCert.value(certNicknameToUse); + bool passok = cert->checkPassword(*password); + while (!passok) { + const QString title = i18n("Enter password (if any) to unlock certificate: %1", certNicknameToUse); + bool ok; + *password = QInputDialog::getText(pageView, i18n("Enter certificate password"), title, QLineEdit::Password, QString(), &ok); + if (ok) { + passok = cert->checkPassword(*password); + } else { + passok = false; + break; + } + } + + if (doc->metaData(QStringLiteral("DocumentHasPassword")).toString() == QLatin1String("yes")) { + *documentPassword = QInputDialog::getText(pageView, i18n("Enter document password"), i18n("Enter document password"), QLineEdit::Password, QString(), &passok); + } + + if (passok) { + certs.removeOne(cert); + } + qDeleteAll(certs); + + return passok ? std::unique_ptr(cert) : std::unique_ptr(); +} + +QString getFileNameForNewSignedFile(PageView *pageView, Okular::Document *doc) +{ + QMimeDatabase db; + const QString typeName = doc->documentInfo().get(Okular::DocumentInfo::MimeType); + const QMimeType mimeType = db.mimeTypeForName(typeName); + const QString mimeTypeFilter = i18nc("File type name and pattern", "%1 (%2)", mimeType.comment(), mimeType.globPatterns().join(QLatin1Char(' '))); + + const QUrl currentFileUrl = doc->currentDocument(); + const QFileInfo currentFileInfo(currentFileUrl.fileName()); + const QString localFilePathIfAny = currentFileUrl.isLocalFile() ? QFileInfo(currentFileUrl.path()).canonicalPath() + QLatin1Char('/') : QString(); + const QString newFileName = + localFilePathIfAny + i18nc("Used when suggesting a new name for a digitally signed file. %1 is the old file name and %2 it's extension", "%1_signed.%2", currentFileInfo.baseName(), currentFileInfo.completeSuffix()); + + return QFileDialog::getSaveFileName(pageView, i18n("Save Signed File As"), newFileName, mimeTypeFilter); +} + +void signUnsignedSignature(const Okular::FormFieldSignature *form, PageView *pageView, Okular::Document *doc) +{ + Q_ASSERT(form && form->signatureType() == Okular::FormFieldSignature::UnsignedSignature); + QString password, documentPassword; + const std::unique_ptr cert = SignatureGuiUtils::getCertificateAndPasswordForSigning(pageView, doc, &password, &documentPassword); + if (!cert) { + return; + } + + Okular::NewSignatureData data; + data.setCertNickname(cert->nickName()); + data.setCertSubjectCommonName(cert->subjectInfo(Okular::CertificateInfo::CommonName)); + data.setPassword(password); + data.setDocumentPassword(documentPassword); + password.clear(); + documentPassword.clear(); + + const QString newFilePath = SignatureGuiUtils::getFileNameForNewSignedFile(pageView, doc); + + if (!newFilePath.isEmpty()) { + const bool success = form->sign(data, newFilePath); + if (success) { + emit pageView->requestOpenFile(newFilePath, form->page()->number() + 1); + } else { + KMessageBox::error(pageView, i18nc("%1 is a file path", "Could not sign. Invalid certificate password or could not write to '%1'", newFilePath)); + } + } +} } diff --git a/part/signatureguiutils.h b/part/signatureguiutils.h index eba06a5db..95fa0bba0 100644 --- a/part/signatureguiutils.h +++ b/part/signatureguiutils.h @@ -11,6 +11,10 @@ #include "core/signatureutils.h" +#include + +class PageView; + namespace Okular { class Document; @@ -30,6 +34,9 @@ QString getReadablePublicKeyType(Okular::CertificateInfo::PublicKeyType type); QString getReadableKeyUsageCommaSeparated(Okular::CertificateInfo::KeyUsageExtensions kuExtensions); QString getReadableKeyUsageNewLineSeparated(Okular::CertificateInfo::KeyUsageExtensions kuExtensions); +std::unique_ptr getCertificateAndPasswordForSigning(PageView *pageView, Okular::Document *doc, QString *password, QString *documentPassword); +QString getFileNameForNewSignedFile(PageView *pageView, Okular::Document *doc); +void signUnsignedSignature(const Okular::FormFieldSignature *form, PageView *pageView, Okular::Document *doc); } #endif diff --git a/part/signaturemodel.cpp b/part/signaturemodel.cpp index 324b2c7b7..98c54643e 100644 --- a/part/signaturemodel.cpp +++ b/part/signaturemodel.cpp @@ -124,32 +124,43 @@ void SignatureModelPrivate::notifySetup(const QVector &pages, in } int revNumber = 1; + int unsignedSignatureNumber = 1; const QVector signatureFormFields = SignatureGuiUtils::getSignatureFormFields(document); for (const Okular::FormFieldSignature *sf : signatureFormFields) { - const Okular::SignatureInfo &info = sf->signatureInfo(); - const int pageNumber = sf->page()->number(); - // based on whether or not signature form is a nullptr it is decided if clicking on an item should change the viewport. - auto *parentItem = new SignatureItem(root, sf, SignatureItem::RevisionInfo, pageNumber); - parentItem->displayString = i18n("Rev. %1: Signed By %2", revNumber, info.signerName()); + if (sf->signatureType() == Okular::FormFieldSignature::UnsignedSignature) { + auto *parentItem = new SignatureItem(root, sf, SignatureItem::RevisionInfo, pageNumber); + parentItem->displayString = i18n("Unsigned Signature %1", unsignedSignatureNumber); - auto childItem1 = new SignatureItem(parentItem, nullptr, SignatureItem::ValidityStatus, pageNumber); - childItem1->displayString = SignatureGuiUtils::getReadableSignatureStatus(info.signatureStatus()); + auto childItem = new SignatureItem(parentItem, sf, SignatureItem::FieldInfo, pageNumber); + childItem->displayString = i18n("Field: %1 on page %2", sf->name(), pageNumber + 1); - auto childItem2 = new SignatureItem(parentItem, nullptr, SignatureItem::SigningTime, pageNumber); - childItem2->displayString = i18n("Signing Time: %1", info.signingTime().toString(Qt::DefaultLocaleLongDate)); + ++unsignedSignatureNumber; + } else { + const Okular::SignatureInfo &info = sf->signatureInfo(); - const QString reason = info.reason(); - if (!reason.isEmpty()) { - auto childItem3 = new SignatureItem(parentItem, nullptr, SignatureItem::Reason, pageNumber); - childItem3->displayString = i18n("Reason: %1", reason); - } + // based on whether or not signature form is a nullptr it is decided if clicking on an item should change the viewport. + auto *parentItem = new SignatureItem(root, sf, SignatureItem::RevisionInfo, pageNumber); + parentItem->displayString = i18n("Rev. %1: Signed By %2", revNumber, info.signerName()); - auto childItem4 = new SignatureItem(parentItem, sf, SignatureItem::FieldInfo, pageNumber); - childItem4->displayString = i18n("Field: %1 on page %2", sf->name(), pageNumber + 1); + auto childItem1 = new SignatureItem(parentItem, nullptr, SignatureItem::ValidityStatus, pageNumber); + childItem1->displayString = SignatureGuiUtils::getReadableSignatureStatus(info.signatureStatus()); - ++revNumber; + auto childItem2 = new SignatureItem(parentItem, nullptr, SignatureItem::SigningTime, pageNumber); + childItem2->displayString = i18n("Signing Time: %1", info.signingTime().toString(Qt::DefaultLocaleLongDate)); + + const QString reason = info.reason(); + if (!reason.isEmpty()) { + auto childItem3 = new SignatureItem(parentItem, nullptr, SignatureItem::Reason, pageNumber); + childItem3->displayString = i18n("Reason: %1", reason); + } + + auto childItem4 = new SignatureItem(parentItem, sf, SignatureItem::FieldInfo, pageNumber); + childItem4->displayString = i18n("Field: %1 on page %2", sf->name(), pageNumber + 1); + + ++revNumber; + } } q->endResetModel(); } diff --git a/part/signaturepanel.cpp b/part/signaturepanel.cpp index 06a779d74..89955be18 100644 --- a/part/signaturepanel.cpp +++ b/part/signaturepanel.cpp @@ -91,9 +91,15 @@ void SignaturePanel::slotShowContextMenu() return; QMenu *menu = new QMenu(this); - QAction *sigProp = new QAction(i18n("Properties"), menu); - connect(sigProp, &QAction::triggered, this, &SignaturePanel::slotViewProperties); - menu->addAction(sigProp); + if (d->m_currentForm->signatureType() == Okular::FormFieldSignature::UnsignedSignature) { + QAction *signAction = new QAction(i18n("&Sign..."), menu); + connect(signAction, &QAction::triggered, this, &SignaturePanel::signUnsignedSignature); + menu->addAction(signAction); + } else { + QAction *sigProp = new QAction(i18n("Properties"), menu); + connect(sigProp, &QAction::triggered, this, &SignaturePanel::slotViewProperties); + menu->addAction(sigProp); + } menu->exec(QCursor::pos()); delete menu; } @@ -105,6 +111,12 @@ void SignaturePanel::slotViewProperties() propDlg.exec(); } +void SignaturePanel::signUnsignedSignature() +{ + Q_D(SignaturePanel); + SignatureGuiUtils::signUnsignedSignature(d->m_currentForm, d->m_pageView, d->m_document); +} + void SignaturePanel::notifySetup(const QVector & /*pages*/, int setupFlags) { if (!(setupFlags & Okular::DocumentObserver::UrlChanged)) diff --git a/part/signaturepanel.h b/part/signaturepanel.h index 457a50adf..a904a4401 100644 --- a/part/signaturepanel.h +++ b/part/signaturepanel.h @@ -40,6 +40,7 @@ private Q_SLOTS: void activated(const QModelIndex &); void slotShowContextMenu(); void slotViewProperties(); + void signUnsignedSignature(); private: Q_DECLARE_PRIVATE(SignaturePanel)