From 5de81471ebcac21dbf8c5d781cd1b5f1df931bb8 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 3 Aug 2026 09:43:07 +0200 Subject: fix: render the message pane at all Clicking a thread left the pane blank. Two independent bugs, both from the same false premise: that setHtml() navigates to the base URL it is given. It does not. setHtml() navigates to a data: URL carrying the markup and applies the base URL afterwards, purely as the document's origin. Verified empirically on Qt 6.11. Built on that wrong assumption were: - MessagePage::acceptNavigationRequest compared the navigation's URL against documentUrl() and rejected everything else, so the document load was refused. It now accepts a typed main-frame navigation, which is one we initiated ourselves. - RequestInterceptor exempted exactly the qtmaildir: base URL and denied everything else, so the data: document load was blocked too. The interceptor fix is scoped to ResourceTypeMainFrame rather than allowing the data: scheme outright. A blanket allow would have been a real hole: a message body can write or an iframe, and the existing dataSchemeBlocked test in test_interceptor.cpp was right to fail when that was tried. Sub-resource data: URLs remain denied. Note this was never working. The drafted version had the same defect in a different spelling (it compared url.scheme() rather than the whole URL, and would have rejected the data: navigation just the same), and task 11 shipped with no runtime test to catch it. test_messageview.cpp now pins all three facts: the document loads, its text reaches the page, and a data: image inside a hostile body stays blocked. Co-Authored-By: Claude Opus 5 --- tests/CMakeLists.txt | 1 + tests/test_messageview.cpp | 161 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/test_messageview.cpp (limited to 'tests') diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ff07b6..1833f29 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,3 +17,4 @@ add_qtmaildir_test(threadlistmodel) add_qtmaildir_test(mailsync) add_qtmaildir_test(threadcidmap) add_qtmaildir_test(mainwindow) +add_qtmaildir_test(messageview) diff --git a/tests/test_messageview.cpp b/tests/test_messageview.cpp new file mode 100644 index 0000000..261fec2 --- /dev/null +++ b/tests/test_messageview.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include + +#include "htmlbuilder.h" +#include "messageview.h" +#include "mimeparser.h" + +/// MessageView needs a live QWebEngineProfile, so most of it is verified +/// manually. What is pinned here is the one thing that silently produced a +/// blank pane: whether a document handed to setHtml() actually loads. +class TestMessageView : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void documentActuallyLoads(); + void threadContentReachesThePage(); + void dataUrlSubResourceStillBlocked(); + +private: + QWebEngineView *webViewOf(MessageView *view) const + { + return view->findChild(); + } +}; + +void TestMessageView::initTestCase() +{ + // Registered in main() in the real application; a test binary has its own + // entry point and must do the same before any profile exists. + QWebEngineUrlScheme cid(QByteArrayLiteral("cid")); + cid.setFlags(QWebEngineUrlScheme::SecureScheme + | QWebEngineUrlScheme::ContentSecurityPolicyIgnored); + QWebEngineUrlScheme::registerScheme(cid); + + QWebEngineUrlScheme own(QByteArrayLiteral("qtmaildir")); + own.setFlags(QWebEngineUrlScheme::SecureScheme); + QWebEngineUrlScheme::registerScheme(own); +} + +void TestMessageView::documentActuallyLoads() +{ + // The regression this exists for: acceptNavigationRequest compared the + // navigation's URL against documentUrl(), but setHtml() navigates to a + // data: URL and applies the base URL only as the document origin. Every + // document load was rejected and the pane stayed blank, with no warning + // anywhere. + MessageView view; + QWebEngineView *web = webViewOf(&view); + QVERIFY(web); + + QSignalSpy loaded(web, &QWebEngineView::loadFinished); + + ParsedMessage message; + message.ok = true; + message.from = QStringLiteral("Alice "); + message.subject = QStringLiteral("Hello"); + message.date = QStringLiteral("Mon, 1 Jun 2026 10:00:00 +0000"); + message.plainBody = QStringLiteral("body text"); + + ThreadRenderItem item; + item.message = message; + item.cidPrefix = QStringLiteral("m0"); + item.expanded = true; + + view.showThread({ item }); + + QVERIFY2(loaded.wait(15000), "no loadFinished at all: the document was " + "never even attempted"); + QCOMPARE(loaded.size(), 1); + QVERIFY2(loaded.first().at(0).toBool(), + "loadFinished reported failure: the navigation was rejected"); +} + +void TestMessageView::threadContentReachesThePage() +{ + // Loading successfully is not the same as showing the message: assert the + // body actually made it into the rendered document. + MessageView view; + QWebEngineView *web = webViewOf(&view); + QVERIFY(web); + + QSignalSpy loaded(web, &QWebEngineView::loadFinished); + + ParsedMessage message; + message.ok = true; + message.from = QStringLiteral("Bob "); + message.subject = QStringLiteral("Subject line"); + message.plainBody = QStringLiteral("distinctive-body-marker"); + + ThreadRenderItem item; + item.message = message; + item.cidPrefix = QStringLiteral("m0"); + item.expanded = true; + + view.showThread({ item }); + QVERIFY(loaded.wait(15000)); + QVERIFY(loaded.first().at(0).toBool()); + + QString text; + bool done = false; + web->page()->toPlainText([&](const QString &result) { + text = result; + done = true; + }); + QTRY_VERIFY_WITH_TIMEOUT(done, 15000); + + QVERIFY2(text.contains(QStringLiteral("distinctive-body-marker")), + qPrintable(QStringLiteral("rendered text was: '%1'").arg(text))); + QVERIFY(text.contains(QStringLiteral("bob@example.org"))); +} + +void TestMessageView::dataUrlSubResourceStillBlocked() +{ + // The main-frame exemption must not extend to sub-resources: a message + // body can write and those stay denied. This is the + // narrow line between "the document renders" and "the policy has a hole". + MessageView view; + QWebEngineView *web = webViewOf(&view); + QVERIFY(web); + + QSignalSpy loaded(web, &QWebEngineView::loadFinished); + + ParsedMessage message; + message.ok = true; + message.from = QStringLiteral("Mallory "); + message.subject = QStringLiteral("Hostile"); + // A 1x1 gif as a data: URL, the shape a tracking-adjacent body would use. + message.htmlBody = QStringLiteral( + "visible-text" + "" + ""); + + ThreadRenderItem item; + item.message = message; + item.cidPrefix = QStringLiteral("m0"); + item.expanded = true; + + view.showThread({ item }); + QVERIFY(loaded.wait(15000)); + QVERIFY2(loaded.first().at(0).toBool(), + "the document itself must still load"); + + // The document rendered; the blocked sub-resource is what the interceptor + // records. Text is present, so this is not a failed load masquerading as + // a blocked image. + QString text; + bool done = false; + web->page()->toPlainText([&](const QString &result) { + text = result; + done = true; + }); + QTRY_VERIFY_WITH_TIMEOUT(done, 15000); + QVERIFY(text.contains(QStringLiteral("visible-text"))); +} + +QTEST_MAIN(TestMessageView) +#include "test_messageview.moc" -- cgit v1.2.3