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) {
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();
198 // qDebug() << "displayUrlWithoutQuery " << displayUrlWithoutQuery << " hrefUrlWithoutQuery " <<
199 // hrefUrlWithoutQuery << " text " << text;
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()
Q_EMITQ_EMIT
T qobject_cast(QObject *object)
qsizetype count() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
StripTrailingSlash
QList< QVariant > toList() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.