MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - HtmlSanitizer.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 85.2 % 61 52
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 8 8
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 37.9 % 140 53

             Branch data     Line data    Source code
       1                 :             : #include "HtmlSanitizer.h"
       2                 :             : 
       3                 :             : #include <QFile>
       4                 :             : #include <QLoggingCategory>
       5                 :             : #include <QWebEnginePage>
       6                 :             : #include <QWebEngineProfile>
       7                 :             : #include <QWebEngineSettings>
       8                 :             : 
       9   [ +  +  +  -  :           5 : Q_LOGGING_CATEGORY(lcSanitizer, "mailjd.sanitizer")
             +  -  -  - ]
      10                 :             : 
      11                 :          10 : HtmlSanitizer::HtmlSanitizer(QObject *parent) : QObject(parent) {}
      12                 :             : 
      13                 :          20 : HtmlSanitizer::~HtmlSanitizer() = default;
      14                 :             : 
      15                 :          10 : void HtmlSanitizer::init() {
      16         [ -  + ]:          10 :   if (m_page)
      17                 :           0 :     return; // Already initialized
      18                 :             : 
      19                 :             :   // Use off-the-record profile (no persistent storage)
      20   [ +  -  +  -  :          10 :   auto *profile = new QWebEngineProfile(this);
             -  +  -  - ]
      21                 :             : 
      22   [ +  -  +  -  :          10 :   m_page = new QWebEnginePage(profile, this);
             -  +  -  - ]
      23                 :             : 
      24                 :             :   // Enable JS on the sanitizer page (needed for DOMPurify)
      25   [ +  -  +  - ]:          10 :   m_page->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
      26                 :             :   // Disable everything else for security
      27   [ +  -  +  - ]:          10 :   m_page->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, false);
      28   [ +  -  +  - ]:          10 :   m_page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false);
      29   [ +  -  +  - ]:          10 :   m_page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
      30                 :             : 
      31                 :             :   // Load DOMPurify from Qt resources
      32         [ +  - ]:          10 :   QFile jsFile(QStringLiteral(":/dompurify/purify.min.js"));
      33   [ +  -  -  + ]:          10 :   if (!jsFile.open(QIODevice::ReadOnly)) {
      34   [ #  #  #  #  :           0 :     qCCritical(lcSanitizer) << "Failed to load DOMPurify resource";
             #  #  #  # ]
      35                 :           0 :     return;
      36                 :             :   }
      37   [ +  -  +  - ]:          10 :   QString dompurifyJs = QString::fromUtf8(jsFile.readAll());
      38                 :             : 
      39                 :             :   // Create minimal HTML page with DOMPurify embedded
      40                 :          20 :   QString html = QStringLiteral(
      41                 :             :       "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
      42                 :             :       "<script>%1</script></head><body></body></html>")
      43         [ +  - ]:          10 :       .arg(dompurifyJs);
      44                 :             : 
      45                 :             :   // Load the page and wait for it to finish
      46                 :          10 :   connect(m_page, &QWebEnginePage::loadFinished, this,
      47         [ +  - ]:          10 :           [this](bool ok) {
      48         [ -  + ]:           5 :     if (!ok) {
      49   [ #  #  #  #  :           0 :       qCCritical(lcSanitizer) << "DOMPurify page load failed";
             #  #  #  # ]
      50                 :           0 :       return;
      51                 :             :     }
      52                 :             : 
      53                 :             :     // Verify DOMPurify is available
      54         [ +  - ]:           5 :     m_page->runJavaScript(
      55                 :          10 :         QStringLiteral("typeof DOMPurify !== 'undefined'"),
      56                 :          10 :         [this](const QVariant &result) {
      57         [ +  - ]:           5 :       if (result.toBool()) {
      58   [ +  -  +  -  :          10 :         qCInfo(lcSanitizer) << "DOMPurify sanitizer ready";
             +  -  +  + ]
      59                 :           5 :         m_ready = true;
      60                 :           5 :         emit ready();
      61                 :             : 
      62                 :             :         // Process any queued request
      63         [ +  + ]:           5 :         if (m_pendingCallback) {
      64         [ +  - ]:           1 :           sanitize(m_pendingHtml, std::move(m_pendingCallback));
      65                 :           1 :           m_pendingHtml.clear();
      66                 :           1 :           m_pendingCallback = nullptr;
      67                 :             :         }
      68                 :             :       } else {
      69   [ #  #  #  #  :           0 :         qCCritical(lcSanitizer) << "DOMPurify not found after page load";
             #  #  #  # ]
      70                 :             :       }
      71                 :           5 :     });
      72                 :             :   });
      73                 :             : 
      74   [ +  -  +  - ]:          10 :   m_page->setHtml(html);
      75         [ +  - ]:          10 : }
      76                 :             : 
      77                 :          17 : void HtmlSanitizer::sanitize(
      78                 :             :     const QString &dirtyHtml,
      79                 :             :     std::function<void(const QString &cleanHtml)> callback) {
      80                 :             : 
      81         [ -  + ]:          17 :   if (!m_page) {
      82   [ #  #  #  #  :           0 :     qCWarning(lcSanitizer) << "Sanitizer not initialized, call init() first";
             #  #  #  # ]
      83         [ #  # ]:           0 :     if (callback)
      84         [ #  # ]:           0 :       callback(QString()); // Return empty to be safe
      85                 :           2 :     return;
      86                 :             :   }
      87                 :             : 
      88         [ +  + ]:          17 :   if (!m_ready) {
      89                 :             :     // Queue the request until DOMPurify is loaded
      90                 :           2 :     m_pendingHtml = dirtyHtml;
      91                 :           2 :     m_pendingCallback = std::move(callback);
      92                 :           2 :     return;
      93                 :             :   }
      94                 :             : 
      95                 :             :   // Escape the HTML for safe embedding in a JS string literal.
      96                 :             :   // We use JSON-style escaping by wrapping in a template.
      97                 :          15 :   QString escaped = dirtyHtml;
      98         [ +  - ]:          15 :   escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
      99         [ +  - ]:          15 :   escaped.replace(QLatin1Char('`'), QStringLiteral("\\`"));
     100         [ +  - ]:          30 :   escaped.replace(QStringLiteral("${"), QStringLiteral("\\${"));
     101                 :             : 
     102                 :             :   // DOMPurify config:
     103                 :             :   // - FORBID_TAGS: dangerous tags that should never appear in email
     104                 :             :   // - FORBID_ATTR: event handlers and other dangerous attributes
     105                 :             :   // - ALLOW_DATA_ATTR: false — no data-* attributes
     106                 :             :   // - ADD_ATTR: allow 'target' for links (mailto: etc.)
     107                 :          30 :   QString js = QStringLiteral(
     108                 :             :       "(function() {"
     109                 :             :       "  try {"
     110                 :             :       "    var dirty = `%1`;"
     111                 :             :       "    var clean = DOMPurify.sanitize(dirty, {"
     112                 :             :       "      FORBID_TAGS: ['style', 'math', 'svg', 'form', 'input',"
     113                 :             :       "                     'textarea', 'select', 'button', 'object',"
     114                 :             :       "                     'embed', 'applet', 'base', 'meta', 'link'],"
     115                 :             :       "      FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover',"
     116                 :             :       "                     'onfocus', 'onblur', 'onsubmit'],"
     117                 :             :       "      ALLOW_DATA_ATTR: false,"
     118                 :             :       "      ADD_ATTR: ['target']"
     119                 :             :       "    });"
     120                 :             :       "    return clean;"
     121                 :             :       "  } catch(e) {"
     122                 :             :       "    return '';"
     123                 :             :       "  }"
     124                 :             :       "})()")
     125         [ +  - ]:          15 :       .arg(escaped);
     126                 :             : 
     127   [ +  -  +  -  :          15 :   m_page->runJavaScript(js, [callback](const QVariant &result) {
                   +  - ]
     128         [ +  - ]:          15 :     QString clean = result.toString();
     129         [ +  - ]:          15 :     if (callback)
     130         [ +  - ]:          15 :       callback(clean);
     131                 :          15 :   });
     132                 :          15 : }
        

Generated by: LCOV version 2.0-1