Messagelib

scamdetectionwebengine.cpp
1 /*
2  SPDX-FileCopyrightText: 2016-2021 Laurent Montel <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 
6 */
7 #include "scamdetectionwebengine.h"
8 #include "MessageViewer/ScamCheckShortUrl"
9 #include "scamcheckshorturlmanager.h"
10 #include "scamdetectiondetailsdialog.h"
11 #include "settings/messageviewersettings.h"
12 #include "webengineviewer/webenginescript.h"
13 #include <WebEngineViewer/WebEngineManageScript>
14 
15 #include <KLocalizedString>
16 
17 #include <QPointer>
18 #include <QRegularExpression>
19 #include <QWebEnginePage>
20 
21 using namespace MessageViewer;
22 
23 template<typename Arg, typename R, typename C> struct 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 
34 template<typename Arg, typename R, typename C>
35 
36 InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFunction)(Arg))
37 {
38  InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFunction};
39  return wrapper;
40 }
41 
42 static QString addWarningColor(const QString &url)
43 {
44  const QString error = QStringLiteral("<font color=#FF0000>%1</font>").arg(url);
45  return error;
46 }
47 
48 class MessageViewer::ScamDetectionWebEnginePrivate
49 {
50 public:
51  ScamDetectionWebEnginePrivate() = default;
52 
53  QString mDetails;
55 };
56 
57 ScamDetectionWebEngine::ScamDetectionWebEngine(QObject *parent)
58  : QObject(parent)
59  , d(new MessageViewer::ScamDetectionWebEnginePrivate)
60 {
61 }
62 
63 ScamDetectionWebEngine::~ScamDetectionWebEngine() = default;
64 
65 void 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 
74 void 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  const QString href = mapVariant.value(QStringLiteral("src")).toString();
94  const QUrl url(href);
95  if (!title.isEmpty()) {
96  if (title.startsWith(QLatin1String("http:")) || title.startsWith(QLatin1String("https:")) || title.startsWith(QLatin1String("www."))) {
97  if (title.startsWith(QLatin1String("www."))) {
98  const QString completUrl = url.scheme() + QLatin1String("://") + title;
99  if (completUrl != href && href != (completUrl + QLatin1Char('/'))) {
100  foundScam = true;
101  }
102  } else {
103  if (href != title) {
104  // http://www.kde.org == http://www.kde.org/
105  if (href != (title + QLatin1Char('/'))) {
106  foundScam = true;
107  }
108  }
109  }
110  if (foundScam) {
111  d->mDetails += QLatin1String("<li>")
112  + 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 "
113  "to mislead the recipient",
114  addWarningColor(title),
115  addWarningColor(href))
116  + QLatin1String("</li>");
117  }
118  }
119  }
120  if (!foundScam) {
121  // 2) detect if url href has ip and not server name.
122  const QString hostname = url.host();
123  if (hostname.contains(ip4regExp) && !hostname.contains(QLatin1String("127.0.0.1"))) { // hostname
124  d->mDetails += QLatin1String("<li>")
125  + i18n("This email contains a link which points to a numerical IP address (%1) instead of a typical textual website address. This is often "
126  "the case in scam emails.",
127  addWarningColor(hostname))
128  + QLatin1String("</li>");
129  foundScam = true;
130  } else if (hostname.contains(QLatin1Char('%'))) { // Hexa value for ip
131  d->mDetails += QLatin1String("<li>")
132  + i18n("This email contains a link which points to a hexadecimal IP address (%1) instead of a typical textual website address. This is "
133  "often the case in scam emails.",
134  addWarningColor(hostname))
135  + QLatin1String("</li>");
136  foundScam = true;
137  } else if (url.toString().contains(QLatin1String("url?q="))) { // 4) redirect url.
138  d->mDetails += QLatin1String("<li>") + i18n("This email contains a link (%1) which has a redirection", addWarningColor(url.toString()))
139  + QLatin1String("</li>");
140  foundScam = true;
141  } else if ((url.toString().count(QStringLiteral("http://")) > 1)
142  || (url.toString().count(QStringLiteral("https://")) > 1)) { // 5) more that 1 http in url.
143  if (!url.toString().contains(QLatin1String("kmail:showAuditLog"))) {
144  d->mDetails += QLatin1String("<li>")
145  + i18n("This email contains a link (%1) which contains multiple http://. This is often the case in scam emails.",
146  addWarningColor(url.toString()))
147  + QLatin1String("</li>");
148  foundScam = true;
149  }
150  }
151  }
152  // Check shortUrl
153  if (!foundScam) {
154  if (ScamCheckShortUrl::isShortUrl(url)) {
155  d->mDetails += QLatin1String("<li>")
156  + i18n("This email contains a shorturl (%1). It can redirect to another server.", addWarningColor(url.toString())) + QLatin1String("</li>");
157  foundScam = true;
158  }
159  }
160  if (!foundScam) {
161  QUrl displayUrl = QUrl(mapVariant.value(QStringLiteral("text")).toString());
162  // Special case if https + port 443 it will return url without port
163  QString text = (displayUrl.port() == 443 && displayUrl.scheme() == QLatin1String("https"))
166  if (text.endsWith(QLatin1String("%22"))) {
167  text.chop(3);
168  }
169  const QUrl normalizedHrefUrl = QUrl(href);
170  QString normalizedHref = normalizedHrefUrl.toDisplayString(QUrl::StripTrailingSlash | QUrl::NormalizePathSegments);
171  if (text != normalizedHref) {
172  if (normalizedHref.contains(QStringLiteral("%5C"))) {
173  normalizedHref.replace(QStringLiteral("%5C"), QStringLiteral("/"));
174  }
175  }
176  // qDebug() << "text " << text << " href "<<href << " normalizedHref " << normalizedHref;
177 
178  if (!text.isEmpty()) {
179  if (text.startsWith(QLatin1String("http:/")) || text.startsWith(QLatin1String("https:/"))) {
180  if (text != normalizedHref) {
181  if (normalizedHref != (text + QLatin1Char('/'))) {
182  if (normalizedHref.toHtmlEscaped() != text) {
183  if (QString::fromUtf8(QUrl(text).toEncoded()) != normalizedHref) {
184  if (QUrl(normalizedHref).toDisplayString() != text) {
185  const bool qurlqueryequal = displayUrl.query() == normalizedHrefUrl.query();
186  const QString displayUrlWithoutQuery =
188  const QString hrefUrlWithoutQuery =
190  // qDebug() << "displayUrlWithoutQuery " << displayUrlWithoutQuery << " hrefUrlWithoutQuery " << hrefUrlWithoutQuery <<
191  // " text " << text;
192  if (qurlqueryequal && (displayUrlWithoutQuery + QLatin1Char('/') != hrefUrlWithoutQuery)) {
193  d->mDetails += QLatin1String("<li>")
194  + i18n("This email contains a link which reads as '%1' in the text, but actually points to '%2'. This is often "
195  "the case in scam emails to mislead the recipient",
196  addWarningColor(text),
197  addWarningColor(normalizedHref))
198  + QLatin1String("</li>");
199  foundScam = true;
200  }
201  }
202  }
203  }
204  }
205  }
206  }
207  }
208  }
209  }
210  if (mapResult.value(QStringLiteral("forms")).toInt() > 0) {
211  d->mDetails += QLatin1String("<li></b>") + i18n("Message contains form element. This is often the case in scam emails.") + QLatin1String("</b></li>");
212  foundScam = true;
213  }
214  if (foundScam) {
215  d->mDetails.prepend(QLatin1String("<b>") + i18n("Details:") + QLatin1String("</b><ul>"));
216  d->mDetails += QLatin1String("</ul>");
217  Q_EMIT messageMayBeAScam();
218  }
219  Q_EMIT resultScanDetection(foundScam);
220 }
221 
222 void ScamDetectionWebEngine::showDetails()
223 {
224  if (!d->mDetailsDialog) {
225  d->mDetailsDialog = new MessageViewer::ScamDetectionDetailsDialog;
226  }
227  d->mDetailsDialog->setDetails(d->mDetails);
228  d->mDetailsDialog->show();
229 }
StripTrailingSlash
QString toDisplayString(QUrl::FormattingOptions options) const const
QList< QVariant > toList() const const
int port(int defaultPort) const const
void chop(int n)
T value(int i) const const
QString fromUtf8(const char *str, int size)
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
QString scheme() const const
QString toHtmlEscaped() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString query(QUrl::ComponentFormattingOptions options) const const
QString i18n(const char *text, const TYPE &arg...)
QString & replace(int position, int n, QChar after)
NETWORKMANAGERQT_EXPORT QString hostname()
int count() const const
const T value(const Key &key, const T &defaultValue) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Fri Nov 26 2021 23:16:43 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.