scamdetectionwebengine.cpp 8.98 KB
Newer Older
1
/*
Laurent Montel's avatar
Laurent Montel committed
2
  Copyright (c) 2016-2018 Montel Laurent <montel@kde.org>
3

Laurent Montel's avatar
Laurent Montel committed
4 5 6 7 8 9 10 11 12
  This library is free software; you can redistribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 of the License, or (at your
  option) any later version.

  This library is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
  License for more details.
13

Laurent Montel's avatar
Laurent Montel committed
14 15 16 17 18 19
  You should have received a copy of the GNU Library General Public License
  along with this library; see the file COPYING.LIB.  If not, write to the
  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  02110-1301, USA.

*/
20
#include "scamdetectionwebengine.h"
21 22
#include "scamcheckshorturlmanager.h"
#include "scamdetectiondetailsdialog.h"
Laurent Montel's avatar
Laurent Montel committed
23
#include "settings/messageviewersettings.h"
Laurent Montel's avatar
Laurent Montel committed
24
#include "MessageViewer/ScamCheckShortUrl"
Laurent Montel's avatar
Laurent Montel committed
25
#include "webengineviewer/webenginescript.h"
26
#include <WebEngineViewer/WebEngineManageScript>
Laurent Montel's avatar
Laurent Montel committed
27 28

#include <KLocalizedString>
29

Laurent Montel's avatar
Laurent Montel committed
30
#include <QRegularExpression>
31
#include <QPointer>
Laurent Montel's avatar
Laurent Montel committed
32
#include <QWebEnginePage>
33 34

using namespace MessageViewer;
Laurent Montel's avatar
Laurent Montel committed
35 36 37

template<typename Arg, typename R, typename C>
struct InvokeWrapper {
38
    QPointer<R> receiver;
Laurent Montel's avatar
Laurent Montel committed
39 40 41
    void (C::*memberFunction)(Arg);
    void operator()(Arg result)
    {
42 43 44
        if (receiver) {
            (receiver->*memberFunction)(result);
        }
Laurent Montel's avatar
Laurent Montel committed
45 46 47 48 49 50 51 52 53 54 55
    }
};

template<typename Arg, typename R, typename C>

InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFunction)(Arg))
{
    InvokeWrapper<Arg, R, C> wrapper = {receiver, memberFunction};
    return wrapper;
}

Laurent Montel's avatar
Laurent Montel committed
56 57 58 59 60
static QString addWarningColor(const QString &url)
{
    const QString error = QStringLiteral("<font color=#FF0000>%1</font>").arg(url);
    return error;
}
61 62 63 64 65 66 67

class MessageViewer::ScamDetectionWebEnginePrivate
{
public:
    ScamDetectionWebEnginePrivate()
    {
    }
Laurent Montel's avatar
Laurent Montel committed
68

69 70
    QString mDetails;
    QPointer<MessageViewer::ScamDetectionDetailsDialog> mDetailsDialog;
71 72 73
};

ScamDetectionWebEngine::ScamDetectionWebEngine(QObject *parent)
Laurent Montel's avatar
Laurent Montel committed
74 75
    : QObject(parent)
    , d(new MessageViewer::ScamDetectionWebEnginePrivate)
76 77 78 79 80 81 82
{
}

ScamDetectionWebEngine::~ScamDetectionWebEngine()
{
    delete d;
}
83

Laurent Montel's avatar
Laurent Montel committed
84 85 86
void ScamDetectionWebEngine::scanPage(QWebEnginePage *page)
{
    if (MessageViewer::MessageViewerSettings::self()->scamDetectionEnabled()) {
87 88 89
        page->runJavaScript(WebEngineViewer::WebEngineScript::findAllAnchorsAndForms(),
                            WebEngineViewer::WebEngineManageScript::scriptWordId(),
                            invoke(this, &ScamDetectionWebEngine::handleScanPage));
Laurent Montel's avatar
Laurent Montel committed
90 91 92 93 94 95
    }
}

void ScamDetectionWebEngine::handleScanPage(const QVariant &result)
{
    bool foundScam = false;
Laurent Montel's avatar
Laurent Montel committed
96

Laurent Montel's avatar
Laurent Montel committed
97
    d->mDetails.clear();
Laurent Montel's avatar
Laurent Montel committed
98 99 100 101 102
    const QVariantList resultList = result.toList();
    if (resultList.count() != 1) {
        Q_EMIT resultScanDetection(foundScam);
        return;
    }
Laurent Montel's avatar
Laurent Montel committed
103
    d->mDetails = QLatin1String("<b>") + i18n("Details:") + QLatin1String("</b><ul>");
Laurent Montel's avatar
Laurent Montel committed
104 105
    QRegularExpression ip4regExp(QStringLiteral(
                                     "\\b[0-9]{1,3}\\.[0-9]{1,3}(?:\\.[0-9]{0,3})?(?:\\.[0-9]{0,3})?"));
Laurent Montel's avatar
Laurent Montel committed
106 107
    const QVariantMap mapResult = resultList.at(0).toMap();
    const QList<QVariant> lst = mapResult.value(QStringLiteral("anchors")).toList();
Laurent Montel's avatar
Laurent Montel committed
108
    for (const QVariant &var : lst) {
Laurent Montel's avatar
Laurent Montel committed
109 110 111
        QMap<QString, QVariant> mapVariant = var.toMap();
        //qDebug()<<" mapVariant"<<mapVariant;

112
        //1) detect if title has a url and title != href
Laurent Montel's avatar
Laurent Montel committed
113 114
        const QString title = mapVariant.value(QStringLiteral("title")).toString();
        const QString href = mapVariant.value(QStringLiteral("src")).toString();
115 116
        const QUrl url(href);
        if (!title.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
117 118 119 120
            if (title.startsWith(QLatin1String("http:"))
                || title.startsWith(QLatin1String("https:"))
                || title.startsWith(QLatin1String("www."))) {
                if (title.startsWith(QLatin1String("www."))) {
Laurent Montel's avatar
Laurent Montel committed
121 122 123
                    const QString completUrl = url.scheme() + QLatin1String("://") + title;
                    if (completUrl != href
                        && href != (completUrl + QLatin1Char('/'))) {
124 125 126 127 128 129 130 131 132 133 134
                        foundScam = true;
                    }
                } else {
                    if (href != title) {
                        // http://www.kde.org == http://www.kde.org/
                        if (href != (title + QLatin1Char('/'))) {
                            foundScam = true;
                        }
                    }
                }
                if (foundScam) {
Laurent Montel's avatar
Laurent Montel committed
135 136 137
                    d->mDetails += QLatin1String("<li>") + 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 to mislead the recipient",
                        addWarningColor(title), addWarningColor(href)) + QLatin1String("</li>");
138 139 140 141 142 143
                }
            }
        }
        if (!foundScam) {
            //2) detect if url href has ip and not server name.
            const QString hostname = url.host();
Laurent Montel's avatar
Laurent Montel committed
144
            if (hostname.contains(ip4regExp) && !hostname.contains(QLatin1String("127.0.0.1"))) { //hostname
Laurent Montel's avatar
Laurent Montel committed
145 146 147
                d->mDetails += QLatin1String("<li>") + i18n(
                    "This email contains a link which points to a numerical IP address (%1) instead of a typical textual website address. This is often the case in scam emails.",
                    addWarningColor(hostname)) + QLatin1String("</li>");
148 149
                foundScam = true;
            } else if (hostname.contains(QLatin1Char('%'))) { //Hexa value for ip
Laurent Montel's avatar
Laurent Montel committed
150 151 152
                d->mDetails += QLatin1String("<li>") + i18n(
                    "This email contains a link which points to a hexadecimal IP address (%1) instead of a typical textual website address. This is often the case in scam emails.",
                    addWarningColor(hostname)) + QLatin1String("</li>");
153
                foundScam = true;
Laurent Montel's avatar
Laurent Montel committed
154
            } else if (url.toString().contains(QLatin1String("url?q="))) { //4) redirect url.
Laurent Montel's avatar
Laurent Montel committed
155 156 157
                d->mDetails += QLatin1String("<li>") + i18n(
                    "This email contains a link (%1) which has a redirection",
                    addWarningColor(url.toString())) + QLatin1String("</li>");
158
                foundScam = true;
Laurent Montel's avatar
Laurent Montel committed
159 160
            } else if ((url.toString().count(QStringLiteral("http://")) > 1)
                       || (url.toString().count(QStringLiteral("https://")) > 1)) { //5) more that 1 http in url.
Laurent Montel's avatar
Laurent Montel committed
161
                if (!url.toString().contains(QLatin1String("kmail:showAuditLog"))) {
Laurent Montel's avatar
Laurent Montel committed
162 163 164
                    d->mDetails += QLatin1String("<li>") + i18n(
                        "This email contains a link (%1) which contains multiple http://. This is often the case in scam emails.",
                        addWarningColor(url.toString())) + QLatin1String("</li>");
165 166 167 168 169 170 171
                    foundScam = true;
                }
            }
        }
        //Check shortUrl
        if (!foundScam) {
            if (ScamCheckShortUrl::isShortUrl(url)) {
Laurent Montel's avatar
Laurent Montel committed
172 173 174
                d->mDetails += QLatin1String("<li>") + i18n(
                    "This email contains a shorturl (%1). It can redirect to another server.", addWarningColor(
                        url.toString())) + QLatin1String("</li>");
175 176 177
                foundScam = true;
            }
        }
178 179 180 181 182
        if (!foundScam) {
            const QString text = mapVariant.value(QStringLiteral("text")).toString();
            if (!text.isEmpty()) {
                if (text.startsWith(QLatin1String("http:/")) || text.startsWith(QLatin1String("https:/"))) {
                    if(text != href) {
Laurent Montel's avatar
Laurent Montel committed
183
                        if (href != (text + QLatin1Char('/'))) {
Laurent Montel's avatar
Laurent Montel committed
184 185 186 187 188 189
                            if (href.toHtmlEscaped() != text) {
                                d->mDetails += QLatin1String("<li>") + 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 to mislead the recipient",
                                            addWarningColor(text), addWarningColor(href)) + QLatin1String("</li>");
                                foundScam = true;
                            }
Laurent Montel's avatar
Laurent Montel committed
190
                        }
191 192 193 194
                    }
                }
            }
        }
195
    }
Laurent Montel's avatar
Laurent Montel committed
196
    if (mapResult.value(QStringLiteral("forms")).toInt() > 0) {
Laurent Montel's avatar
Laurent Montel committed
197 198 199
        d->mDetails += QLatin1String("<li></b>") + i18n(
            "Message contains form element. This is often the case in scam emails.")
                       + QLatin1String("</b></li>");
200 201
        foundScam = true;
    }
Laurent Montel's avatar
Laurent Montel committed
202
    d->mDetails += QLatin1String("</ul>");
Laurent Montel's avatar
Laurent Montel committed
203
    // qDebug()<<" d->mDetails "<< d->mDetails;
Laurent Montel's avatar
Laurent Montel committed
204 205 206
    if (foundScam) {
        Q_EMIT messageMayBeAScam();
    }
207
    Q_EMIT resultScanDetection(foundScam);
Laurent Montel's avatar
Laurent Montel committed
208
}
209 210 211 212 213 214 215 216 217

void ScamDetectionWebEngine::showDetails()
{
    if (!d->mDetailsDialog) {
        d->mDetailsDialog = new MessageViewer::ScamDetectionDetailsDialog;
    }
    d->mDetailsDialog->setDetails(d->mDetails);
    d->mDetailsDialog->show();
}