/* SPDX-FileCopyrightText: 2018 Chinmoy Ranjan Pradhan SPDX-License-Identifier: GPL-2.0-or-later */ #include "signaturepartutils.h" #include "core/document.h" #include "core/form.h" #include "core/page.h" #include "pageview.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace SignaturePartUtils { std::optional getCertificateAndPasswordForSigning(PageView *pageView, Okular::Document *doc) { const Okular::CertificateStore *certStore = doc->certificateStore(); bool userCancelled, nonDateValidCerts; QList certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts); if (userCancelled) { return std::nullopt; } if (certs.isEmpty()) { pageView->showNoSigningCertificatesDialog(nonDateValidCerts); return std::nullopt; } QString password; QString documentPassword; QStandardItemModel items; QHash nickToCert; int minWidth = -1; for (auto cert : qAsConst(certs)) { auto item = std::make_unique(); QString commonName = cert->subjectInfo(Okular::CertificateInfo::CommonName); item->setData(commonName, Qt::UserRole); QString emailAddress = cert->subjectInfo(Okular::CertificateInfo::EmailAddress); item->setData(emailAddress, Qt::UserRole + 1); minWidth = std::max(minWidth, emailAddress.size() + commonName.size()); item->setData(cert->nickName(), Qt::DisplayRole); item->setData(cert->subjectInfo(Okular::CertificateInfo::DistinguishedName), Qt::ToolTipRole); items.appendRow(item.release()); nickToCert[cert->nickName()] = cert; } SelectCertificateDialog dialog(pageView); dialog.combo->setMinimumContentsLength(minWidth + 5); dialog.combo->setModel(&items); auto result = dialog.exec(); if (result == QDialog::Rejected) { qDeleteAll(certs); return std::nullopt; } auto certNicknameToUse = dialog.combo->currentText(); // 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 (!passok) { return std::nullopt; } 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); if (passok) { return SigningInformation {std::unique_ptr(cert), password, documentPassword}; } return std::nullopt; } 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); const std::optional signingInfo = getCertificateAndPasswordForSigning(pageView, doc); if (!signingInfo) { return; } Okular::NewSignatureData data; data.setCertNickname(signingInfo->certificate->nickName()); data.setCertSubjectCommonName(signingInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName)); data.setPassword(signingInfo->certificatePassword); data.setDocumentPassword(signingInfo->documentPassword); const QString newFilePath = getFileNameForNewSignedFile(pageView, doc); if (!newFilePath.isEmpty()) { const bool success = form->sign(data, newFilePath); if (success) { Q_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)); } } } SelectCertificateDialog::SelectCertificateDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18n("Select certificate to sign with")); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); combo = new QComboBox(); combo->setItemDelegate(new KeyDelegate); auto layout = new QVBoxLayout(); layout->addWidget(new QLabel(i18n("Certificates:"))); layout->addWidget(combo); layout->addWidget(buttonBox); setLayout(layout); } QSize KeyDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { auto baseSize = QStyledItemDelegate::sizeHint(option, index); baseSize.setHeight(baseSize.height() * 2); return baseSize; } void KeyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto style = option.widget ? option.widget->style() : QApplication::style(); QStyledItemDelegate::paint(painter, option, QModelIndex()); // paint the background but without any text on it. if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.color(QPalette::HighlightedText)); } else { painter->setPen(option.palette.color(QPalette::Text)); } auto textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &option); int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, option.widget) + 1; textRect.adjust(textMargin, 0, -textMargin, 0); QRect topHalf {textRect.x(), textRect.y(), textRect.width(), textRect.height() / 2}; QRect bottomHalf {textRect.x(), textRect.y() + textRect.height() / 2, textRect.width(), textRect.height() / 2}; style->drawItemText(painter, topHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignLeft, option.palette, true, index.data(Qt::DisplayRole).toString()); style->drawItemText(painter, bottomHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignRight, option.palette, true, index.data(Qt::UserRole + 1).toString()); style->drawItemText(painter, bottomHalf, (option.displayAlignment & Qt::AlignVertical_Mask) | Qt::AlignLeft, option.palette, true, index.data(Qt::UserRole).toString()); } }