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 : : }
|