MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 27.4% 86 / 0 / 314
Functions: 28.6% 12 / 0 / 42
Branches: 15.6% 86 / 0 / 551

quick/QuickHtmlReader.cpp
Line Branch Exec Source
1 #include "QuickHtmlReader.h"
2 #include "util/HostMatch.h"
3 #include "presentation/MailReaderState.h"
4 #include "presentation/DesignTokens.h"
5 #include "presentation/WorkspaceState.h"
6 #include "app/core/MailSessionCore.h"
7 #include "app/core/AccountSession.h"
8 #include "data/MailCache.h"
9 #include "service/HtmlInlineImageService.h"
10 #include "ui/HtmlSanitizer.h"
11 #include "performance/MailOpenPerformanceTracker.h"
12 #include "ui/HtmlDocumentDelivery.h"
13 #include "ui/ExternalContentInterceptor.h"
14 #include "ui/ExternalContentPolicy.h"
15 #include <QColor>
16 #include <QDataStream>
17 #include <QQmlComponent>
18 #include <QQmlEngine>
19 #include <QQuickWebEngineProfile>
20 #include <QLoggingCategory>
21 #include <QQuickWebEngineDownloadRequest>
22 #include <QGuiApplication>
23 #include <QClipboard>
24 #include <QFutureWatcher>
25 #include <QtConcurrent>
26
27 ✗ Q_LOGGING_CATEGORY(lcQuickHtml, "mailjd.quick.html")
28
29 795 QuickHtmlReader::QuickHtmlReader(MailReaderState &reader, MailSessionCore &session, DesignTokens &tokens, WorkspaceState &workspace, QObject *parent)
30 795 : QObject(parent), m_reader(reader), m_session(session), m_tokens(tokens), m_workspace(workspace),
31
10/16
✓ Branch 6 → 7 taken 795 times.
✓ Branch 7 → 8 taken 795 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 795 times.
✓ Branch 10 → 11 taken 795 times.
✓ Branch 11 → 12 taken 795 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 795 times.
✓ Branch 15 → 16 taken 795 times.
✓ Branch 16 → 17 taken 795 times.
✓ Branch 17 → 18 taken 795 times.
✓ Branch 20 → 21 taken 795 times.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
795 m_delivery(new HtmlDocumentDelivery(this)), m_interceptor(new ExternalContentInterceptor(this)) {
32
1/1
✓ Branch 24 → 25 taken 795 times.
795 reader.setHtmlRenderer(this);
33
2/2
✓ Branch 26 → 27 taken 795 times.
✓ Branch 27 → 28 taken 795 times.
795 m_interceptor->setDocumentPolicy({}, true, {});
34
1/1
✓ Branch 30 → 31 taken 795 times.
795 m_retention.setSingleShot(true);
35
1/1
✓ Branch 31 → 32 taken 795 times.
795 m_retention.setInterval(5000);
36
1/1
✓ Branch 32 → 33 taken 795 times.
795 m_timeout.setSingleShot(true);
37
1/1
✓ Branch 33 → 34 taken 795 times.
795 m_timeout.setInterval(30000);
38
1/1
✓ Branch 34 → 35 taken 795 times.
795 m_envelopePool.setMaxThreadCount(1);
39
1/1
✓ Branch 35 → 36 taken 795 times.
795 m_envelopePool.setExpiryTimeout(-1);
40
1/1
✓ Branch 36 → 37 taken 795 times.
795 connect(&m_retention, &QTimer::timeout, this, &QuickHtmlReader::retire);
41
1/3
✗ Branch 2 → 3 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 38 → 39 taken 795 times.
795 connect(&m_timeout, &QTimer::timeout, this, [this] { fail(tr("The HTML reader did not finish loading. Retry.")); });
42
1/1
✓ Branch 40 → 41 taken 795 times.
795 connect(&reader, &MailReaderState::changed, this, &QuickHtmlReader::synchronize);
43
1/1
✓ Branch 42 → 43 taken 795 times.
795 connect(&workspace, &WorkspaceState::activeTabChanged, this, [this] {
44
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 512 times.
512 if (m_tabId == m_workspace.activeTabId()) return;
45 // A logical owner can change without another body delivery or mode change.
46 // Revoke the old owner's rights even when both tabs display the same mail.
47
5/12
✓ Branch 8 → 9 taken 512 times.
✓ Branch 9 → 10 taken 512 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 185 times.
✓ Branch 23 → 24 taken 512 times.
✗ Branch 23 → 25 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 512 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
1209 if (WorkspaceState::decode(m_reader.identity()) == m_workspace.mailId() &&
48
5/10
✓ Branch 11 → 12 taken 185 times.
✓ Branch 11 → 18 taken 327 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 185 times.
✓ Branch 19 → 21 taken 327 times.
✓ Branch 21 → 22 taken 512 times.
✗ Branch 21 → 23 not taken.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
1209 m_workspace.readingMode() == "html" && m_reader.htmlRequested()) start();
49 512 else invalidate();
50 });
51
2/3
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1072 times.
✓ Branch 44 → 45 taken 795 times.
1867 connect(&tokens, &DesignTokens::changed, this, [this] { if (m_reader.htmlRequested()) start(); });
52
1/1
✓ Branch 47 → 48 taken 795 times.
795 connect(session.accounts(), &AccountSession::accountsLoaded, this, [this] {
53
1/1
✓ Branch 2 → 3 taken 793 times.
793 const QPointer<QuickHtmlReader> guard(this);
54
1/1
✓ Branch 3 → 4 taken 793 times.
793 m_workspace.clearHtmlConsent();
55 793 m_allowExternal = false;
56
1/1
✓ Branch 4 → 5 taken 793 times.
793 invalidate();
57
2/3
✓ Branch 6 → 7 taken 793 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 8 taken 793 times.
793 if (guard) retire();
58 793 });
59
1/1
✓ Branch 49 → 50 taken 795 times.
795 connect(m_interceptor, &ExternalContentInterceptor::documentExternalContentBlocked, this, [this](const QUrl &url) {
60 ✗ if (url != m_url || !m_reader.htmlRequested()) return;
61 ✗ m_blocked = true;
62 ✗ emit changed();
63 });
64 // Policy writers (settings surfaces, whitelist editors) publish changes
65 // through the shared policy/cache signals; the running document refreshes
66 // within its existing consent and document-token boundaries.
67
1/1
✓ Branch 52 → 53 taken 795 times.
795 connect(m_session.cache(), &MailCache::whitelistChanged, this, &QuickHtmlReader::refreshPolicy);
68
1/1
✓ Branch 55 → 56 taken 795 times.
795 connect(m_session.cache(), &MailCache::folderIdentityInvalidated, this,
69 795 [this](qint64 folderId) {
70
4/4
✓ Branch 2 → 3 taken 16 times.
✓ Branch 3 → 4 taken 16 times.
✓ Branch 6 → 7 taken 10 times.
✓ Branch 6 → 8 taken 6 times.
16 if (WorkspaceState::decode(m_reader.identity()).folderId == folderId)
71 10 invalidate();
72 16 });
73
2/2
✓ Branch 57 → 58 taken 795 times.
✓ Branch 58 → 59 taken 795 times.
795 connect(ExternalContentPolicy::instance(), &ExternalContentPolicy::changed, this, &QuickHtmlReader::refreshPolicy);
74 795 }
75 795 QuickHtmlReader::~QuickHtmlReader() {
76 795 invalidate();
77 1590 m_envelopePool.waitForDone();
78 795 m_reader.setHtmlRenderer(nullptr);
79 795 releaseProfile();
80 795 }
81 ✗ QObject *QuickHtmlReader::profile() const { return m_profile.data(); }
82 8691 void QuickHtmlReader::invalidate() {
83
1/1
✓ Branch 2 → 3 taken 8691 times.
8691 const QPointer<QuickHtmlReader> guard(this);
84 8691 ++m_lease;
85
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 8691 times.
8691 if (m_activeEnvelopeCancelled) m_activeEnvelopeCancelled->store(true);
86 8691 m_pendingEnvelope.reset();
87
2/2
✓ Branch 8 → 9 taken 8691 times.
✓ Branch 9 → 10 taken 8691 times.
8691 HtmlInlineImageService::shared()->cancel(this);
88
1/3
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 8691 times.
✗ Branch 13 → 14 not taken.
8691 if (m_sanitizer) m_sanitizer->cancel(this);
89
1/1
✓ Branch 14 → 15 taken 8691 times.
8691 m_timeout.stop();
90
2/2
✓ Branch 16 → 17 taken 8691 times.
✓ Branch 17 → 18 taken 8691 times.
8691 m_interceptor->setDocumentPolicy({}, true, {});
91
1/1
✓ Branch 20 → 21 taken 8691 times.
8691 m_delivery->clear();
92 8691 m_ready = m_blocked = false;
93 8691 m_restoringScroll = false;
94 8691 m_leaseUidValidity = 0;
95
1/1
✓ Branch 21 → 22 taken 8691 times.
8691 const bool hadUrl = !m_url.isEmpty();
96
1/1
✓ Branch 22 → 23 taken 8691 times.
8691 m_url = QUrl();
97
1/1
✓ Branch 25 → 26 taken 8691 times.
8691 emit changed();
98
3/7
✓ Branch 27 → 28 taken 8691 times.
✗ Branch 27 → 30 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 8691 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 8691 times.
✗ Branch 32 → 33 not taken.
8691 if (guard && hadUrl) emit documentUrlChanged();
99 8691 }
100 6839 void QuickHtmlReader::synchronize() {
101
2/2
✓ Branch 3 → 4 taken 258 times.
✓ Branch 3 → 5 taken 6581 times.
6839 if (m_reader.renderRevision() == m_revision) return;
102 6581 m_revision = m_reader.renderRevision();
103 6581 m_allowExternal = false;
104
1/2
✓ Branch 7 → 8 taken 6581 times.
✗ Branch 7 → 19 not taken.
6581 if (!m_reader.htmlRequested()) {
105
1/1
✓ Branch 8 → 9 taken 6581 times.
6581 const QPointer<QuickHtmlReader> guard(this);
106
1/1
✓ Branch 9 → 10 taken 6581 times.
6581 invalidate();
107
3/7
✓ Branch 11 → 12 taken 6581 times.
✗ Branch 11 → 14 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 6581 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 6581 times.
✗ Branch 16 → 17 not taken.
6581 if (guard && m_resident) m_retention.start();
108 6581 return;
109 6581 }
110 ✗ start();
111 }
112 ✗ bool QuickHtmlReader::current(quint64 lease) const {
113 ✗ const auto id = WorkspaceState::decode(m_reader.identity());
114 ✗ return lease == m_lease && m_revision == m_reader.renderRevision() && m_reader.htmlRequested() &&
115 ✗ m_tabId == m_workspace.activeTabId() && id == m_workspace.mailId() &&
116 ✗ m_accountGeneration == m_session.accounts()->generation() && id.isValid() &&
117 ✗ m_leaseUidValidity == m_reader.body().cacheUidValidity;
118 ✗ }
119 ✗ void QuickHtmlReader::start() {
120 ✗ const QPointer<QuickHtmlReader> guard(this);
121 ✗ invalidate();
122 ✗ if (!guard) return;
123 ✗ qCDebug(lcQuickHtml) << "Starting display lease" << m_lease << "revision" << m_revision;
124 ✗ m_retention.stop();
125 ✗ if (!m_reader.htmlRequested()) return;
126 // Resident full-HTML reading keeps the DOMPurify page warm until retire().
127 ✗ m_sanitizer = HtmlSanitizer::shared();
128 ✗ m_sanitizer->holdWarm(this, true);
129 // The first lease check still reads the authoritative cache epoch. Later
130 // callbacks use this snapshot; setUidValidity revokes it synchronously
131 // before any purge or epoch write instead of re-querying SQLite per check.
132 ✗ const auto id = WorkspaceState::decode(m_reader.identity());
133 ✗ m_leaseUidValidity = m_session.cache()->uidValidity(id.folderId);
134 ✗ const auto lease = m_lease;
135 ✗ m_tabId = m_workspace.activeTabId();
136 ✗ m_accountGeneration = m_session.accounts()->generation();
137 ✗ m_reader.prepareHtml(m_revision);
138 ✗ if (!guard || !current(lease)) return;
139 ✗ const HtmlInlineImageRequest input{m_session.cache()->databasePath(), WorkspaceState::decode(m_reader.identity()),
140 ✗ m_reader.body().cacheUidValidity, m_reader.body().textHtml};
141 ✗ HtmlInlineImageService::shared()->load(this, input, [this, lease, input](HtmlInlineImageResult result) {
142 ✗ if (!current(lease)) return;
143 ✗ if (!result.error.isEmpty()) { fail(tr("Could not prepare HTML images: %1").arg(result.error)); return; }
144 ✗ const QPointer<QuickHtmlReader> guard(this);
145 ✗ m_digest = result.contentDigest;
146 ✗ const auto state = m_workspace.bindHtmlDocument(m_tabId, input.mailId, input.uidValidity, m_digest);
147 ✗ if (!guard || !current(lease)) return;
148 ✗ m_allowExternal = state.allowExternal;
149 ✗ m_scrollTarget = state.scroll;
150 ✗ const bool profileReady = ensureProfile();
151 ✗ if (!guard || !current(lease)) return;
152 ✗ if (!profileReady) { fail(tr("The HTML rendering profile could not be created. Retry.")); return; }
153 // P90.7 trace: the inline-image preparation finished right here.
154 {
155 ✗ const auto traceId = WorkspaceState::decode(m_reader.identity());
156 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance()
157 ✗ .inlinePreparationFinished(traceId.folderId, traceId.uid);
158 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance()
159 ✗ .sanitizerStarted(traceId.folderId, traceId.uid);
160 ✗ }
161 ✗ m_sanitizer = HtmlSanitizer::shared();
162 ✗ QByteArray identity;
163 ✗ QDataStream stream(&identity, QIODevice::WriteOnly);
164 ✗ stream << input.databasePath << input.mailId.accountId << input.mailId.folderId << input.mailId.uid << input.uidValidity;
165 ✗ m_sanitizer->sanitize(input.expectedHtml, result.images, [this, lease](const QString &clean) {
166 ✗ if (!current(lease)) return;
167 {
168 ✗ const auto traceId = WorkspaceState::decode(m_reader.identity());
169 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance()
170 ✗ .sanitizerFinished(traceId.folderId, traceId.uid, true);
171 ✗ }
172 ✗ if (clean.isEmpty()) { fail(tr("The HTML message could not be sanitized. Retry.")); return; }
173 ✗ queueEnvelope(clean, lease);
174 }, this, identity);
175 ✗ });
176 ✗ }
177 ✗ void QuickHtmlReader::queueEnvelope(QString clean, quint64 lease) {
178 ✗ const auto colors = m_tokens.palette();
179 ✗ const auto color = [&](const char *key) {
180 ✗ return QColor(colors.value(QString::fromLatin1(key)).toString()).name();
181 ✗ };
182 ✗ m_pendingEnvelope = EnvelopeJob{std::move(clean), color("surface"), color("text"),
183 color("accent"), lease,
184 ✗ std::make_shared<std::atomic_bool>(false)};
185 ✗ dispatchEnvelope();
186 ✗ }
187 ✗ void QuickHtmlReader::dispatchEnvelope() {
188 ✗ if (m_envelopeBusy || !m_pendingEnvelope) return;
189 ✗ auto job = std::move(*m_pendingEnvelope);
190 ✗ m_pendingEnvelope.reset();
191 ✗ if (!current(job.lease)) return;
192 ✗ m_envelopeBusy = true;
193 ✗ m_activeEnvelopeCancelled = job.cancelled;
194 ✗ const quint64 lease = job.lease;
195 ✗ auto *watcher = new QFutureWatcher<QByteArray>(this);
196 ✗ connect(watcher, &QFutureWatcher<QByteArray>::finished, this, [this, watcher, lease] {
197 ✗ const QPointer<QuickHtmlReader> guard(this);
198 ✗ const QByteArray html = watcher->result();
199 ✗ watcher->deleteLater();
200 ✗ m_envelopeBusy = false;
201 ✗ m_activeEnvelopeCancelled.reset();
202 ✗ if (current(lease)) {
203 ✗ if (html.isEmpty()) {
204 ✗ fail(tr("The HTML message exceeds the display limit."));
205 } else {
206 ✗ const auto url = m_delivery->publish(html);
207 ✗ if (url.isEmpty()) {
208 ✗ fail(tr("The HTML message exceeds the display limit."));
209 } else {
210 ✗ const bool block = !m_allowExternal && ExternalContentPolicy::instance()->shouldBlock();
211 ✗ m_interceptor->setDocumentPolicy(url, block, m_session.cache()->allowlist().whitelistedDomains());
212 ✗ if (!guard) return;
213 ✗ if (current(lease)) {
214 ✗ m_url = url;
215 ✗ m_timeout.start();
216 ✗ emit documentUrlChanged();
217 }
218 }
219 ✗ }
220 }
221 ✗ if (guard) dispatchEnvelope();
222 ✗ });
223 ✗ watcher->setFuture(QtConcurrent::run(&m_envelopePool, [job = std::move(job)]() -> QByteArray {
224 ✗ if (job.cancelled->load()) return {};
225 ✗ const QString envelope = QStringLiteral("<!DOCTYPE html><html><head><meta charset='utf-8'>"
226 "<meta name='referrer' content='no-referrer'>"
227 "<meta http-equiv='Content-Security-Policy' content=\"default-src 'none'; base-uri 'none'; "
228 "form-action 'none'; style-src 'unsafe-inline'; img-src data: https: http:;\">"
229 "<style>html,body{background:%1;color:%2}body{font-family:sans-serif;margin:8px}"
230 "a[href]{cursor:pointer;color:%3}</style></head><body>%4</body></html>")
231 ✗ .arg(job.surface, job.text, job.accent, job.clean);
232 ✗ if (job.cancelled->load()) return {};
233 ✗ QByteArray encoded = envelope.toUtf8();
234 ✗ if (job.cancelled->load() || encoded.size() > HtmlDocumentDelivery::MaxDocumentBytes)
235 ✗ return {};
236 ✗ return encoded;
237 ✗ }));
238 ✗ }
239 9 void QuickHtmlReader::refreshPolicy() {
240 // Only the running document reacts; stale leases, foreign tabs and
241 // account-generation changes are handled by invalidate()/synchronize().
242
3/10
✓ Branch 2 → 3 taken 9 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 9 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 8 not taken.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 9 times.
✗ Branch 10 → 12 not taken.
9 if (m_url.isEmpty() || !m_reader.htmlRequested() || !current(m_lease)) return;
243 ✗ const auto blockedHosts = m_interceptor->blockedDomains();
244 ✗ const auto domains = m_session.cache()->allowlist().whitelistedDomains();
245 ✗ const bool block = !m_allowExternal && ExternalContentPolicy::instance()->shouldBlock();
246 ✗ m_interceptor->setDocumentPolicy(m_url, block, domains);
247 ✗ if (!m_blocked) return;
248 // Denied requests stay denied inside the running document; only a fresh
249 // lease re-requests them. It re-binds the same snapshot digest, so per-tab
250 // consent and scroll position survive. A global permission unblocks every
251 // denied host; a whitelist entry must cover one of the blocked hosts.
252 // An entry covers its subdomains like the interceptor does (67.A4):
253 // allowing "example.com" loads a blocked "img.example.com".
254 ✗ const auto covered = [&domains](const QString &host) {
255 ✗ for (const QString &entry : domains)
256 ✗ if (HostMatch::isHostOrSubdomain(host, entry)) return true;
257 ✗ return false;
258 ✗ };
259 ✗ bool loadable = !block;
260 ✗ if (block) {
261 ✗ for (const QString &host : blockedHosts) {
262 ✗ if (covered(host)) { loadable = true; break; }
263 }
264 }
265 ✗ if (loadable) start();
266 ✗ }
267
268 ✗ QStringList QuickHtmlReader::blockedDomains() const {
269 ✗ if (!m_blocked || !m_interceptor) return {};
270 ✗ QStringList hosts(m_interceptor->blockedDomains().cbegin(), m_interceptor->blockedDomains().cend());
271 ✗ hosts.sort();
272 ✗ return hosts;
273 ✗ }
274 ✗ bool QuickHtmlReader::ensureProfile() {
275 ✗ const QPointer<QuickHtmlReader> guard(this);
276 ✗ if (!m_profile) {
277 ✗ if (!m_engine) return false;
278 ✗ QQmlComponent component(m_engine);
279 ✗ component.setData("import QtWebEngine\nWebEngineProfilePrototype {}", QUrl{});
280 ✗ m_prototype.reset(component.create());
281 ✗ QQuickWebEngineProfile *profile = nullptr;
282 ✗ if (!m_prototype || !QMetaObject::invokeMethod(m_prototype.get(), "instance", Q_RETURN_ARG(QQuickWebEngineProfile *, profile)) || !profile)
283 ✗ return false;
284 ✗ m_profile = profile;
285 ✗ QQmlEngine::setObjectOwnership(profile, QQmlEngine::CppOwnership);
286 ✗ profile->setOffTheRecord(true);
287 ✗ profile->setHttpCacheType(QQuickWebEngineProfile::NoCache);
288 ✗ profile->setPersistentCookiesPolicy(QQuickWebEngineProfile::NoPersistentCookies);
289 ✗ profile->setUrlRequestInterceptor(m_interceptor);
290 ✗ profile->installUrlSchemeHandler(HtmlDocumentDelivery::Scheme, m_delivery);
291 ✗ connect(profile, &QQuickWebEngineProfile::downloadRequested, this, [](QQuickWebEngineDownloadRequest *request) { request->cancel(); });
292 ✗ emit profileChanged();
293 ✗ if (!guard) return false;
294 ✗ }
295 ✗ m_resident = true;
296 ✗ emit changed();
297 ✗ return true;
298 ✗ }
299 ✗ void QuickHtmlReader::loaded(const QUrl &url, bool ok) {
300 ✗ qCDebug(lcQuickHtml) << "Display load completed" << "success" << ok << "currentUrl" << (url == m_url)
301 ✗ << "currentLease" << current(m_lease) << "ready" << m_ready;
302 ✗ if (!url.isEmpty() && url == m_url) {
303 // P90.7 trace: the document load finished for this lease (the probe
304 // classifies the generation itself; a stale load cancels, not fails).
305 ✗ const auto traceId = WorkspaceState::decode(m_reader.identity());
306 ✗ MailJD::Performance::MailOpenPerformanceTracker::instance()
307 ✗ .webEngineLoadFinished(traceId.folderId, traceId.uid, ok, current(m_lease));
308 ✗ }
309 ✗ if (url.isEmpty() || url != m_url || !current(m_lease)) return;
310 ✗ if (ok && m_ready) return;
311 ✗ if (!ok) { fail(tr("The HTML message could not be displayed. Retry.")); return; }
312 ✗ m_timeout.stop();
313 ✗ m_ready = true;
314 ✗ m_restoringScroll = true;
315 ✗ const QPointer<QuickHtmlReader> guard(this);
316 ✗ m_reader.finishHtml(m_revision, {});
317 ✗ if (!guard || !isCurrentDocument(url)) return;
318 ✗ emit changed();
319 ✗ if (guard && isCurrentDocument(url)) emit restoreScrollRequested(url, m_scrollTarget);
320 ✗ }
321 ✗ void QuickHtmlReader::fail(const QString &error) {
322 ✗ const QPointer<QuickHtmlReader> guard(this);
323 ✗ const auto revision = m_revision;
324 ✗ qCWarning(lcQuickHtml) << "Display lease failed:" << error;
325 ✗ invalidate();
326 ✗ if (guard) m_reader.finishHtml(revision, error);
327 ✗ }
328 ✗ void QuickHtmlReader::crashed() {
329 ✗ if (!m_reader.htmlRequested()) return;
330 ✗ const QPointer<QuickHtmlReader> guard(this);
331 ✗ fail(tr("The HTML renderer stopped. Retry."));
332 ✗ if (!guard) return;
333 ✗ const auto failedLease = m_lease;
334 // Chromium still owns the emitting view's stack while reporting its crash.
335 // Destroy the view/profile only after that callback has completely unwound.
336 ✗ QTimer::singleShot(0, this, [this, failedLease] { if (m_lease == failedLease) retire(); });
337 ✗ }
338 ✗ void QuickHtmlReader::allowExternal() {
339 ✗ if (!m_reader.htmlRequested() || !m_blocked) return;
340 ✗ if (!m_workspace.allowHtmlExternal(m_tabId, WorkspaceState::decode(m_reader.identity()),
341 ✗ m_reader.body().cacheUidValidity, m_digest)) return;
342 ✗ m_allowExternal = true;
343 ✗ start();
344 }
345 ✗ void QuickHtmlReader::openLink(const QUrl &url, bool userInitiated) {
346 ✗ if (!userInitiated || !isCurrentDocument(m_url) || !canOpenLink(url)) return;
347 ✗ emit externalLinkRequested(url);
348 }
349 ✗ bool QuickHtmlReader::isCurrentDocument(const QUrl &document) const {
350 ✗ return m_ready && !document.isEmpty() && document == m_url && current(m_lease);
351 }
352 ✗ bool QuickHtmlReader::canOpenLink(const QUrl &url) const {
353 ✗ return ExternalContentPolicy::canOpenExternalLink(url);
354 }
355 ✗ bool QuickHtmlReader::copyLink(const QUrl &document, const QUrl &url) {
356 ✗ if (!isCurrentDocument(document) || !canOpenLink(url)) return false;
357 ✗ QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded));
358 ✗ return true;
359 }
360 ✗ void QuickHtmlReader::rememberScroll(const QUrl &document, const QPointF &position) {
361 ✗ if (m_restoringScroll || !isCurrentDocument(document)) return;
362 ✗ m_workspace.rememberHtmlScroll(m_tabId, WorkspaceState::decode(m_reader.identity()),
363 ✗ m_reader.body().cacheUidValidity, m_digest, position);
364 }
365 ✗ void QuickHtmlReader::scrollRestored(const QUrl &document) {
366 ✗ if (isCurrentDocument(document)) m_restoringScroll = false;
367 ✗ }
368 793 void QuickHtmlReader::retire() {
369
1/1
✓ Branch 2 → 3 taken 793 times.
793 const QPointer<QuickHtmlReader> guard(this);
370
1/1
✓ Branch 3 → 4 taken 793 times.
793 m_retention.stop();
371 793 m_resident = false;
372
1/3
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 793 times.
✗ Branch 7 → 8 not taken.
793 if (m_sanitizer) m_sanitizer->holdWarm(this, false);
373
1/1
✓ Branch 8 → 9 taken 793 times.
793 emit changed();
374
4/7
✓ Branch 2 → 3 taken 737 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 737 times.
✗ Branch 3 → 5 not taken.
✓ Branch 10 → 11 taken 793 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 793 times.
1530 if (guard) QTimer::singleShot(0, this, [this] { if (!m_resident && !m_liveViews) releaseProfile(); });
375 793 }
376 ✗ void QuickHtmlReader::viewReleased() {
377 ✗ m_liveViews = qMax(0, m_liveViews - 1);
378 // Chromium and QML finish destroying the view before retiring its profile.
379 ✗ QTimer::singleShot(0, this, [this] { if (!m_resident && !m_liveViews) releaseProfile(); });
380 ✗ }
381 1532 void QuickHtmlReader::releaseProfile() {
382
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1532 times.
1532 if (m_profile) {
383 ✗ m_profile->setUrlRequestInterceptor(nullptr);
384 ✗ m_profile->removeUrlSchemeHandler(m_delivery);
385 }
386 1532 m_prototype.reset();
387 1532 m_profile = nullptr;
388 1532 emit profileChanged();
389 1532 }
390