Messagelib

scamdetectionwebengine.cpp
1/*
2 SPDX-FileCopyrightText: 2016-2024 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5
6*/
7#include "scamdetectionwebengine.h"
8#include "MessageViewer/ScamCheckShortUrl"
9#include "scamdetectiondetailsdialog.h"
10#include "settings/messageviewersettings.h"
11#include "webengineviewer/webenginescript.h"
12#include <WebEngineViewer/WebEngineManageScript>
13
14#include <KLocalizedString>
15
16#include <QPointer>
17#include <QRegularExpression>
18#include <QWebEnginePage>
19
20using namespace MessageViewer;
21
22template<typename Arg, typename R, typename C>
23struct InvokeWrapper {
24 QPointer<R> receiver;
25 void (C::*memberFunction)(Arg);
26 void operator()(Arg result)
27 {
28 if (receiver) {
29 (receiver->*memberFunction)(result);
30 }
31 }
32};
33
34template<typename Arg, typename R, typename C>
35
36InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFunction)(Arg))
37{
38 InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFunction};
39 return wrapper;
40}
41
42static QString addWarningColor(const QString &url)
43{
44 const QString error = QStringLiteral("<font color=#FF0000>%1</font>").arg(url);
45 return error;
46}
47
48class MessageViewer::ScamDetectionWebEnginePrivate
49{
50public:
51 ScamDetectionWebEnginePrivate() = default;
52
53 QString mDetails;
55};
56
57ScamDetectionWebEngine::ScamDetectionWebEngine(QObject *parent)
58 : QObject(parent)
59 , d(new MessageViewer::ScamDetectionWebEnginePrivate)
60{
61}
62
63ScamDetectionWebEngine::~ScamDetectionWebEngine() = default;
64
65void ScamDetectionWebEngine::scanPage(QWebEnginePage *page)
66{
67 if (MessageViewer::MessageViewerSettings::self()->scamDetectionEnabled()) {
68 page->runJavaScript(WebEngineViewer::WebEngineScript::findAllAnchorsAndForms(),
69 WebEngineViewer::WebEngineManageScript::scriptWordId(),
70 invoke(this, &ScamDetectionWebEngine::handleScanPage));
71 }
72}
73
74void ScamDetectionWebEngine::handleScanPage(const QVariant &result)
75{
76 bool foundScam = false;
77
78 d->mDetails.clear();
79 const QVariantList resultList = result.toList();
80 if (resultList.count() != 1) {
81 Q_EMIT resultScanDetection(foundScam);
82 return;
83 }
84 static const QRegularExpression ip4regExp(QStringLiteral("\\b[0-9]{1,3}\\.[0-9]{1,3}(?:\\.[0-9]{0,3})?(?:\\.[0-9]{0,3})?"));
85 const QVariantMap mapResult = resultList.at(0).toMap();
86 const QList<QVariant> lst = mapResult.value(QStringLiteral("anchors")).toList();
87 for (const QVariant &var : lst) {
88 QMap<QString, QVariant> mapVariant = var.toMap();
89 // qDebug()<<" mapVariant"<<mapVariant;
90
91 // 1) detect if title has a url and title != href
92 const QString title = mapVariant.value(QStringLiteral("title")).toString();
93 QString href = mapVariant.value(QStringLiteral("src")).toString();
94 if (!QUrl(href).toString().contains(QLatin1StringView("kmail:showAuditLog"))) {
95 href = href.toLower();
96 }
97 const QUrl url(href);
98 if (!title.isEmpty()) {
99 if (title.startsWith(QLatin1StringView("http:")) || title.startsWith(QLatin1StringView("https:")) || title.startsWith(QLatin1StringView("www."))) {
100 if (title.startsWith(QLatin1StringView("www."))) {
101 const QString completUrl = url.scheme() + QLatin1StringView("://") + title;
102 if (completUrl != href && href != (completUrl + QLatin1Char('/'))) {
103 foundScam = true;
104 }
105 } else {
106 if (href != title) {
107 // http://www.kde.org == http://www.kde.org/
108 if (href != (title + QLatin1Char('/'))) {
109 foundScam = true;
110 }
111 }
112 }
113 if (foundScam) {
114 d->mDetails += QLatin1StringView("<li>")
115 + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is often the case in scam emails "
116 "to mislead the recipient",
117 addWarningColor(title),
118 addWarningColor(href))
119 + QLatin1StringView("</li>");
120 }
121 }
122 }
123 if (!foundScam) {
124 // 2) detect if url href has ip and not server name.
125 const QString hostname = url.host();
126 if (hostname.contains(ip4regExp) && !hostname.contains(QLatin1StringView("127.0.0.1"))) { // hostname
127 d->mDetails += QLatin1StringView("<li>")
128 + i18n("This email contains a link which points to a numerical IP address (%1) instead of a typical textual website address. This is often "
129 "the case in scam emails.",
130 addWarningColor(hostname))
131 + QLatin1StringView("</li>");
132 foundScam = true;
133 } else if (hostname.contains(QLatin1Char('%'))) { // Hexa value for ip
134 d->mDetails += QLatin1StringView("<li>")
135 + i18n("This email contains a link which points to a hexadecimal IP address (%1) instead of a typical textual website address. This is "
136 "often the case in scam emails.",
137 addWarningColor(hostname))
138 + QLatin1StringView("</li>");
139 foundScam = true;
140 } else if (url.toString().contains(QLatin1StringView("url?q="))) { // 4) redirect url.
141 d->mDetails += QLatin1StringView("<li>") + i18n("This email contains a link (%1) which has a redirection", addWarningColor(url.toString()))
142 + QLatin1StringView("</li>");
143 foundScam = true;
144 } else if ((url.toString().count(QStringLiteral("http://")) > 1)
145 || (url.toString().count(QStringLiteral("https://")) > 1)) { // 5) more that 1 http in url.
146 if (!url.toString().contains(QLatin1StringView("kmail:showAuditLog"))) {
147 d->mDetails += QLatin1StringView("<li>")
148 + i18n("This email contains a link (%1) which contains multiple http://. This is often the case in scam emails.",
149 addWarningColor(url.toString()))
150 + QLatin1StringView("</li>");
151 foundScam = true;
152 }
153 }
154 }
155 // Check shortUrl
156 if (!foundScam) {
157 if (ScamCheckShortUrl::isShortUrl(url)) {
158 d->mDetails += QLatin1StringView("<li>")
159 + i18n("This email contains a shorturl (%1). It can redirect to another server.", addWarningColor(url.toString()))
160 + QLatin1StringView("</li>");
161 foundScam = true;
162 }
163 }
164 if (!foundScam) {
165 QUrl displayUrl = QUrl(mapVariant.value(QStringLiteral("text")).toString());
166 // Special case if https + port 443 it will return url without port
167 QString text = (displayUrl.port() == 443 && displayUrl.scheme() == QLatin1StringView("https"))
170 if (text.endsWith(QLatin1StringView("%22"))) {
171 text.chop(3);
172 }
173 const QUrl normalizedHrefUrl = QUrl(href.toLower());
175 if (text != normalizedHref) {
176 if (normalizedHref.contains(QStringLiteral("%5C"))) {
177 normalizedHref.replace(QStringLiteral("%5C"), QStringLiteral("/"));
178 }
179 }
180 if (normalizedHref.endsWith(QLatin1StringView("%22"))) {
181 normalizedHref.chop(3);
182 }
183 // qDebug() << "text " << text << " href "<<href << " normalizedHref " << normalizedHref;
184
185 if (!text.isEmpty()) {
186 if (text.startsWith(QLatin1StringView("http:/")) || text.startsWith(QLatin1StringView("https:/"))) {
187 if (text.toLower() != normalizedHref.toLower()) {
188 if (text != normalizedHref) {
189 if (normalizedHref != (text + QLatin1Char('/'))) {
190 if (normalizedHref.toHtmlEscaped() != text) {
191 if (QString::fromUtf8(QUrl(text).toEncoded()) != normalizedHref) {
192 if (QUrl(normalizedHref).toDisplayString() != text) {
193 const bool qurlqueryequal = displayUrl.query() == normalizedHrefUrl.query();
194 const QString displayUrlWithoutQuery =
196 const QString hrefUrlWithoutQuery =
198 // qDebug() << "displayUrlWithoutQuery " << displayUrlWithoutQuery << " hrefUrlWithoutQuery " <<
199 // hrefUrlWithoutQuery << " text " << text;
200 if (qurlqueryequal && (displayUrlWithoutQuery + QLatin1Char('/') != hrefUrlWithoutQuery)) {
201 d->mDetails += QLatin1StringView("<li>")
202 + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is "
203 "often "
204 "the case in scam emails to mislead the recipient",
205 addWarningColor(text),
206 addWarningColor(normalizedHref))
207 + QLatin1StringView("</li>");
208 foundScam = true;
209 }
210 }
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218 }
219 }
220 if (mapResult.value(QStringLiteral("forms")).toInt() > 0) {
221 d->mDetails +=
222 QLatin1StringView("<li></b>") + i18n("Message contains form element. This is often the case in scam emails.") + QLatin1StringView("</b></li>");
223 foundScam = true;
224 }
225 if (foundScam) {
226 d->mDetails.prepend(QLatin1StringView("<b>") + i18n("Details:") + QLatin1StringView("</b><ul>"));
227 d->mDetails += QLatin1StringView("</ul>");
228 Q_EMIT messageMayBeAScam();
229 }
230 Q_EMIT resultScanDetection(foundScam);
231}
232
233void ScamDetectionWebEngine::showDetails()
234{
235 if (!d->mDetailsDialog) {
236 d->mDetailsDialog = new MessageViewer::ScamDetectionDetailsDialog;
237 }
238 d->mDetailsDialog->setDetails(d->mDetails);
239 d->mDetailsDialog->show();
240}
241
242#include "moc_scamdetectionwebengine.cpp"
constexpr bool isEmpty() const
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
NETWORKMANAGERQT_EXPORT QString hostname()
T value(const Key &key, const T &defaultValue) const const
Q_EMITQ_EMIT
qsizetype count() const const
void chop(qsizetype n)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toHtmlEscaped() const const
QString toLower() const const
StripTrailingSlash
int port(int defaultPort) const const
QString query(ComponentFormattingOptions options) const const
QString scheme() const const
QString toDisplayString(FormattingOptions options) const const
QList< QVariant > toList() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:08:46 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.