MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - PdfViewerWidget.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 45.8 % 118 54
Test Date: 2026-07-27 17:53:44 Functions: 53.8 % 13 7
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 24.7 % 288 71

             Branch data     Line data    Source code
       1                 :             : #include "PdfViewerWidget.h"
       2                 :             : 
       3                 :             : #include "util/AttachmentFileSecurity.h"
       4                 :             : 
       5                 :             : #include <QDir>
       6                 :             : #include <QEvent>
       7                 :             : #include <QFileDialog>
       8                 :             : #include <QHBoxLayout>
       9                 :             : #include <QLabel>
      10                 :             : #include <QLoggingCategory>
      11                 :             : #include <QMessageBox>
      12                 :             : #include <QPdfDocument>
      13                 :             : #include <QPdfView>
      14                 :             : #include <QStandardPaths>
      15                 :             : #include <QTemporaryFile>
      16                 :             : #include <QToolButton>
      17                 :             : #include <QVBoxLayout>
      18                 :             : 
      19                 :             : #include <utility>
      20                 :             : 
      21   [ #  #  #  #  :           0 : Q_LOGGING_CATEGORY(lcPdfViewer, "mailjd.pdfviewer")
             #  #  #  # ]
      22                 :             : 
      23         [ +  - ]:          90 : PdfViewerWidget::PdfViewerWidget(QWidget *parent) : QWidget(parent) {
      24   [ +  -  +  -  :          90 :   auto *layout = new QVBoxLayout(this);
             -  +  -  - ]
      25         [ +  - ]:          90 :   layout->setContentsMargins(0, 0, 0, 0);
      26         [ +  - ]:          90 :   layout->setSpacing(0);
      27                 :             : 
      28                 :             :   // --- Toolbar ---
      29   [ +  -  +  -  :          90 :   auto *toolbar = new QHBoxLayout();
             -  +  -  - ]
      30         [ +  - ]:          90 :   toolbar->setContentsMargins(6, 2, 6, 2);
      31         [ +  - ]:          90 :   toolbar->setSpacing(4);
      32                 :             : 
      33   [ +  -  +  -  :          90 :   auto *backBtn = m_backBtn = new QToolButton(this);
             -  +  -  - ]
      34   [ +  -  +  - ]:          90 :   backBtn->setText(tr("← Back"));
      35   [ +  -  +  - ]:          90 :   backBtn->setToolTip(tr("Back to message"));
      36         [ +  - ]:          90 :   backBtn->setAutoRaise(true);
      37         [ +  - ]:          90 :   connect(backBtn, &QToolButton::clicked, this, &PdfViewerWidget::backRequested);
      38         [ +  - ]:          90 :   toolbar->addWidget(backBtn);
      39                 :             : 
      40   [ +  -  +  -  :          90 :   auto *saveBtn = m_saveBtn = new QToolButton(this);
             -  +  -  - ]
      41   [ +  -  +  - ]:          90 :   saveBtn->setText(tr("Save…"));
      42   [ +  -  +  - ]:          90 :   saveBtn->setToolTip(tr("Save PDF to disk"));
      43         [ +  - ]:          90 :   saveBtn->setAutoRaise(true);
      44         [ +  - ]:          90 :   connect(saveBtn, &QToolButton::clicked, this, [this]() {
      45         [ #  # ]:           0 :     if (m_currentData.isEmpty()) return;
      46                 :             :     // SEC-2026-07-21-08: route the PDF save through the same hardened
      47                 :             :     // AttachmentFileSecurity path as the AttachmentBar: normalize the
      48                 :             :     // filename, write atomically with exact byte check, fsync, and 0600
      49                 :             :     // permissions. The previous inline QSaveFile code did not check the
      50                 :             :     // write() return value (short-write → corrupt file reported as
      51                 :             :     // success), did not normalize the filename, and used default 0666
      52                 :             :     // permissions.
      53                 :             :     const QString normalized = AttachmentFileSecurity::normalizedFileName(
      54   [ #  #  #  #  :           0 :         m_currentFilename.isEmpty() ? QStringLiteral("document.pdf")
                   #  # ]
      55         [ #  # ]:           0 :                                     : m_currentFilename);
      56                 :           0 :     const QString path = QFileDialog::getSaveFileName(
      57   [ #  #  #  # ]:           0 :         this, tr("Save Attachment"), normalized);
      58         [ #  # ]:           0 :     if (path.isEmpty()) return;
      59                 :           0 :     QString errorMessage;
      60         [ #  # ]:           0 :     if (!AttachmentFileSecurity::writeAtomically(
      61         [ #  # ]:           0 :             path, m_currentData, &errorMessage)) {
      62         [ #  # ]:           0 :       QMessageBox::warning(
      63         [ #  # ]:           0 :           this, tr("Save Error"),
      64   [ #  #  #  # ]:           0 :           tr("Could not save %1: %2").arg(path, errorMessage));
      65                 :             :     }
      66   [ #  #  #  # ]:           0 :   });
      67         [ +  - ]:          90 :   toolbar->addWidget(saveBtn);
      68                 :             : 
      69         [ +  - ]:          90 :   toolbar->addStretch();
      70                 :             : 
      71                 :             :   // Zoom controls.
      72   [ +  -  +  -  :          90 :   auto *zoomOutBtn = m_zoomOutBtn = new QToolButton(this);
             -  +  -  - ]
      73         [ +  - ]:         180 :   zoomOutBtn->setText(QStringLiteral("−"));
      74   [ +  -  +  - ]:          90 :   zoomOutBtn->setToolTip(tr("Zoom out"));
      75         [ +  - ]:          90 :   zoomOutBtn->setAutoRaise(true);
      76                 :          90 :   connect(zoomOutBtn, &QToolButton::clicked, this,
      77         [ +  - ]:          90 :           [this]() { changeZoom(-25); });
      78         [ +  - ]:          90 :   toolbar->addWidget(zoomOutBtn);
      79                 :             : 
      80   [ +  -  +  -  :         180 :   m_zoomLabel = new QLabel(QStringLiteral("100%"), this);
             -  +  -  - ]
      81         [ +  - ]:          90 :   m_zoomLabel->setMinimumWidth(48);
      82         [ +  - ]:          90 :   m_zoomLabel->setAlignment(Qt::AlignCenter);
      83         [ +  - ]:          90 :   toolbar->addWidget(m_zoomLabel);
      84                 :             : 
      85   [ +  -  +  -  :          90 :   auto *zoomInBtn = m_zoomInBtn = new QToolButton(this);
             -  +  -  - ]
      86         [ +  - ]:         180 :   zoomInBtn->setText(QStringLiteral("+"));
      87   [ +  -  +  - ]:          90 :   zoomInBtn->setToolTip(tr("Zoom in"));
      88         [ +  - ]:          90 :   zoomInBtn->setAutoRaise(true);
      89                 :          90 :   connect(zoomInBtn, &QToolButton::clicked, this,
      90         [ +  - ]:          90 :           [this]() { changeZoom(25); });
      91         [ +  - ]:          90 :   toolbar->addWidget(zoomInBtn);
      92                 :             : 
      93         [ +  - ]:          90 :   layout->addLayout(toolbar);
      94                 :             : 
      95                 :             :   // T-71.6: QPdfView/QPdfDocument are created lazily in ensureView() on the
      96                 :             :   // first load() call. This keeps MailView construction (and thus every mail
      97                 :             :   // open) fast — no PDF engine init until a PDF is actually viewed.
      98                 :          90 : }
      99                 :             : 
     100                 :         180 : PdfViewerWidget::~PdfViewerWidget() = default;
     101                 :             : 
     102                 :        1687 : void PdfViewerWidget::changeEvent(QEvent *event) {
     103         [ +  + ]:        1687 :   if (event->type() == QEvent::LanguageChange)
     104                 :           8 :     retranslateUi();
     105                 :        1687 :   QWidget::changeEvent(event);
     106                 :        1687 : }
     107                 :             : 
     108                 :           8 : void PdfViewerWidget::retranslateUi() {
     109   [ +  -  +  - ]:           8 :   m_backBtn->setText(tr("← Back"));
     110   [ +  -  +  - ]:           8 :   m_backBtn->setToolTip(tr("Back to message"));
     111   [ +  -  +  - ]:           8 :   m_saveBtn->setText(tr("Save…"));
     112   [ +  -  +  - ]:           8 :   m_saveBtn->setToolTip(tr("Save PDF to disk"));
     113   [ +  -  +  - ]:           8 :   m_zoomOutBtn->setToolTip(tr("Zoom out"));
     114   [ +  -  +  - ]:           8 :   m_zoomInBtn->setToolTip(tr("Zoom in"));
     115                 :           8 : }
     116                 :             : 
     117                 :           0 : void PdfViewerWidget::ensureView() {
     118         [ #  # ]:           0 :   if (m_pdfView) return;
     119                 :             : 
     120   [ #  #  #  #  :           0 :   m_document = new QPdfDocument(this);
                   #  # ]
     121   [ #  #  #  #  :           0 :   m_pdfView = new QPdfView(this);
                   #  # ]
     122                 :           0 :   m_pdfView->setDocument(m_document);
     123                 :           0 :   m_pdfView->setZoomMode(QPdfView::ZoomMode::Custom);
     124                 :           0 :   m_pdfView->setZoomFactor(1.0);
     125         [ #  # ]:           0 :   qobject_cast<QVBoxLayout *>(layout())->addWidget(m_pdfView, 1);
     126                 :             : }
     127                 :             : 
     128                 :           0 : void PdfViewerWidget::load(const QByteArray &data,
     129                 :             :                            const QString &filename) {
     130         [ #  # ]:           0 :   ensureView();
     131                 :           0 :   m_currentData = data;
     132                 :           0 :   m_currentFilename = filename;
     133         [ #  # ]:           0 :   setZoomDisplay(100);
     134         [ #  # ]:           0 :   m_pdfView->setZoomFactor(1.0);
     135                 :             : 
     136                 :             :   // Write to a temp file with .pdf extension (QPdfDocument::load takes a
     137                 :             :   // file path, not a byte array).
     138                 :             :   // SEC-2026-07-21-08/21: Use the application config directory instead of
     139                 :             :   // QDir::temp() (/tmp), following the "no mail data in /tmp" policy
     140                 :             :   // (DatabaseSecurity.h). Also verify the write succeeded so a short write
     141                 :             :   // does not produce a silently corrupt PDF view.
     142         [ #  # ]:           0 :   delete m_tempFile;
     143                 :             :   const QString cacheDir =
     144         [ #  # ]:           0 :       QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
     145   [ #  #  #  # ]:           0 :   QDir().mkpath(cacheDir);
     146         [ #  # ]:           0 :   m_tempFile = new QTemporaryFile(
     147   [ #  #  #  # ]:           0 :       QDir(cacheDir).absoluteFilePath(QStringLiteral("mailjd_pdf_XXXXXX.pdf")),
     148   [ #  #  #  #  :           0 :       this);
                   #  # ]
     149   [ #  #  #  # ]:           0 :   if (!m_tempFile->open()) {
     150   [ #  #  #  #  :           0 :     qCWarning(lcPdfViewer) << "cannot create temp file";
             #  #  #  # ]
     151                 :           0 :     return;
     152                 :             :   }
     153   [ #  #  #  # ]:           0 :   if (m_tempFile->write(data) != static_cast<qint64>(data.size())) {
     154   [ #  #  #  #  :           0 :     qCWarning(lcPdfViewer) << "short write on temp PDF file";
             #  #  #  # ]
     155         [ #  # ]:           0 :     delete m_tempFile;
     156                 :           0 :     m_tempFile = nullptr;
     157                 :           0 :     return;
     158                 :             :   }
     159         [ #  # ]:           0 :   m_tempFile->close();
     160                 :             : 
     161   [ #  #  #  # ]:           0 :   m_document->load(m_tempFile->fileName());
     162         [ #  # ]:           0 : }
     163                 :             : 
     164                 :           0 : void PdfViewerWidget::clear() {
     165                 :           0 :   m_currentData.clear();
     166                 :           0 :   m_currentFilename.clear();
     167                 :           0 :   setZoomDisplay(100);
     168         [ #  # ]:           0 :   if (m_pdfView) m_pdfView->setZoomFactor(1.0);
     169         [ #  # ]:           0 :   if (m_document) m_document->close();
     170         [ #  # ]:           0 :   delete m_tempFile;
     171                 :           0 :   m_tempFile = nullptr;
     172                 :           0 : }
     173                 :             : 
     174                 :           0 : void PdfViewerWidget::setZoomDisplay(int percent) {
     175                 :           0 :   m_zoomPercent = percent;
     176   [ #  #  #  # ]:           0 :   m_zoomLabel->setText(QStringLiteral("%1%").arg(percent));
     177                 :           0 : }
     178                 :             : 
     179                 :           0 : void PdfViewerWidget::changeZoom(int deltaPercent) {
     180                 :           0 :   int next = m_zoomPercent + deltaPercent;
     181         [ #  # ]:           0 :   next = qBound(25, next, 500);
     182         [ #  # ]:           0 :   if (next == m_zoomPercent) return;
     183         [ #  # ]:           0 :   setZoomDisplay(next);
     184         [ #  # ]:           0 :   m_pdfView->setZoomFactor(next / 100.0);
     185                 :             : }
        

Generated by: LCOV version 2.0-1