/*
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2008 Dirk Mueller <mueller@kde.org>
    SPDX-FileCopyrightText: 2008-2010 Urs Wolfer <uwolfer @ kde.org>
    SPDX-FileCopyrightText: 2009 Dawit Alemayehu <adawit@kde.org>

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

#include "webenginepage.h"

#include "webenginepart.h"
#include "webengineview.h"
#include "settings/webenginesettings.h"
#include "webenginepartdownloadmanager.h"
#include "wallet/webenginewallet.h"
#include <webenginepart_debug.h>
#include "webenginepartcontrols.h"
#include "navigationrecorder.h"
#include "profile.h"
#include "webenginepart_ext.h"
#include "capturesourcechooserdlg.h"

#include "libkonq_utils.h"
#include "interfaces/browser.h"

#include <QWebEngineCertificateError>
#include <QWebEngineSettings>
#include <QWebEngineProfile>
#include <KDialogJobUiDelegate>
#include <QWebEngineView>

#if QT_VERSION >= QT_VERSION_CHECK(6,7,0)
#include <QWebEngineDesktopMediaRequest>
#endif

#include <KMessageBox>
#include <KLocalizedString>
#include <KShell>
#include <KAuthorized>
#include <KStringHandler>
#include <KUrlAuthorized>
#include <KSharedConfig>
#include <KIO/AuthInfo>
#include <KIO/Job>
#include <KIO/CommandLauncherJob>
#include <KJobTrackerInterface>
#include <KUserTimestamp>
#include <KPasswdServerClient>
#include <KJobWidgets>
#include <KPluginMetaData>
#include <KIO/JobUiDelegateFactory>

#include <QStandardPaths>
#include <QScreen>
#include <QFileDialog>
#include <QDialogButtonBox>
#include <QMimeDatabase>
#include <QWebEngineNewWindowRequest>

#include <QFile>
#include <QAuthenticator>
#include <QApplication>
#include <QNetworkReply>
#include <QTimer>
#include <QWebEngineHistory>
#include <QWebEngineHistoryItem>
#include <QUrlQuery>
#include <QWebEngineDownloadRequest>
#include <KConfigGroup>
#include <KToggleFullScreenAction>
//#include <QWebSecurityOrigin>
#include "utils.h"
#include "htmlextension.h"

using namespace KonqInterfaces;
using namespace WebEngine;

using ReqNavigationType = QWebEngineNavigationRequest::NavigationType;

WebEnginePage::WebEnginePage(WebEnginePart *part, QWidget *parent)
    : QWebEnginePage(KonqWebEnginePart::Profile::defaultProfile(), parent),
        m_kioErrorCode(0),
        m_ignoreError(false),
        m_part(part),
        m_passwdServerClient(new KPasswdServerClient),
        m_dropOperationTimer(new QTimer(this))
{
    if (view()) {
        WebEngineSettings::self()->computeFontSizes(view()->logicalDpiY());
    }

    connect(this, &QWebEnginePage::navigationRequested, this, &WebEnginePage::handleNavigationRequest);
    connect(this, &QWebEnginePage::newWindowRequested, this, &WebEnginePage::createNewWindow);
    connect(this, &QWebEnginePage::geometryChangeRequested,
            this, &WebEnginePage::slotGeometryChangeRequested);
    connect(this, &QWebEnginePage::featurePermissionRequested,
            this, &WebEnginePage::slotFeaturePermissionRequested);
    connect(this, &QWebEnginePage::loadFinished,
            this, &WebEnginePage::slotLoadFinished);
    connect(this, &QWebEnginePage::authenticationRequired,
            this, &WebEnginePage::slotAuthenticationRequired);
    connect(this, &QWebEnginePage::fullScreenRequested, this, &WebEnginePage::changeFullScreenMode);
    connect(this, &QWebEnginePage::recommendedStateChanged, this, &WebEnginePage::changeLifecycleState);
    connect(this, &QWebEnginePage::printRequested, this, &WebEnginePage::print);

    connect(this, &QWebEnginePage::loadingChanged, this, [this](const QWebEngineLoadingInfo &info) {m_loadStatus = info.status();});

#ifdef WEBENGINE_FRAMES_SUPPORTED
    connect(this, &QWebEnginePage::printRequestedByFrame, this, &WebEnginePage::printFrame);
#endif

#ifdef MEDIAREQUEST_SUPPORTED
    connect(this, &QWebEnginePage::desktopMediaRequested, this, &WebEnginePage::chooseDesktopMedia);
#endif

    connect(this, &QWebEnginePage::loadStarted, this, [this](){m_dropOperationTimer->stop();});
    m_dropOperationTimer->setSingleShot(true);

    connect(this, &QWebEnginePage::certificateError, this, &WebEnginePage::handleCertificateError);

//If this part is displaying the developer tools for another part, inform the other page it's not displaying the developer tools anymore.
//I'm not sure this is needed, but I think it's better to do it, just to be on the safe side
    auto unsetInspectedPageIfNeeded = [this](bool ok) {
        if (ok && inspectedPage() && url().scheme() != QLatin1String("devtools")) {
            setInspectedPage(nullptr);
        }
    };
    connect(this, &QWebEnginePage::loadFinished, this, unsetInspectedPageIfNeeded);

    controls()->navigationRecorder()->registerPage(this);
    m_part->downloadManager()->addPage(this);

    setBackgroundColor(WebEngineSettings::self()->customBackgroundColor());
    connect(controls(), &WebEnginePartControls::updateBackgroundColor, this, [this](const QColor &color){setBackgroundColor(color);});
    connect(controls(), &WebEnginePartControls::updateStyleSheet, this, &WebEnginePage::updateUserStyleSheet);
}

WebEnginePage::~WebEnginePage()
{
//qCDebug(WEBENGINEPART_LOG) << this;
}

inline WebEnginePartControls* WebEnginePage::controls() const
{
    return WebEnginePartControls::self();
}

const WebSslInfo& WebEnginePage::sslInfo() const
{
    return m_sslInfo;
}

QWidget *WebEnginePage::view() const
{
    return QWebEngineView::forPage(this);
}

void WebEnginePage::setSslInfo (const WebSslInfo& info)
{
    m_sslInfo = info;
}

static std::optional<QString> checkForDownloadManager(QWidget* widget)
{
    KConfigGroup cfg (KSharedConfig::openConfig(QStringLiteral("konquerorrc"), KConfig::NoGlobals), "HTML Settings");
    const QString fileName (cfg.readPathEntry("DownloadManager", QString()));
    if (fileName.isEmpty()) {
        return std::nullopt;
    }

    const QString exeName = QStandardPaths::findExecutable(fileName);
    if (exeName.isEmpty()) {
        KMessageBox::detailedError(widget,
                                    i18n("The download manager (%1) could not be found in your installation.", fileName),
                                    i18n("Try to reinstall it and make sure that it is available in $PATH. \n\nThe integration will be disabled."));
        cfg.writePathEntry("DownloadManager", QString());
        cfg.sync();
        return std::nullopt;
    }
    return exeName;
}

bool WebEnginePage::downloadWithExternalDonwloadManager(const QUrl &url)
{
    if (url.isLocalFile()) {
        return false;
    }

    auto useDlManager = checkForDownloadManager(view());
    if (!useDlManager) {
        return false;
    }
    QString downloadManagerExe = useDlManager.value();

    //qCDebug(WEBENGINEPART_LOG) << "Calling command" << cmd;
    KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(downloadManagerExe, {url.toString()});
    job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, view()));
    job->start();
    return true;
}

void WebEnginePage::requestDownload(QWebEngineDownloadRequest *item, bool newWindow, WebEnginePartDownloadManager::DownloadObjective objective)
{
    QUrl downloadUrl = item->url();
    if (downloadWithExternalDonwloadManager(downloadUrl)) {
        item->cancel();
        item->deleteLater();
        return;
    }

    WebEngineDownloadJob *job = new WebEngineDownloadJob(item, nullptr);
    BrowserArguments bArgs;
    bArgs.setForcesNewWindow(newWindow);
    bArgs.setSuggestedDownloadName(item->suggestedFileName());
    bArgs.setDownloadJob(job);
    if (Konq::Settings::alwaysEmbedInNewTab()) {
        bArgs.setNewTab(true);
    }

    //Ensure that if the user is pressing the CTRL button, the URL is downloaded
    //in another window/tab. Usually, QWebEnginePage handles it automatically,
    //but only when clicking on a link. This ensures the same behavior also when
    //the download is started in other ways, for example by clicking on a button.
    //We only do this if url() is empty because when it's empty it means that
    //QWebEnginePage is already handling the CTRL button and this is the new empty
    //page corresponding to the new tab, so we have nothing to do
    if (!url().isEmpty()) {
        Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
        if ((modifiers & Qt::ControlModifier)) {
            bArgs.setForcesNewWindow(true);
            bArgs.setNewTab(true);
        }
    }

    KParts::OpenUrlArguments args;

    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForName(item->mimeType());
    if (!mime.isValid() || mime.isDefault()) {
        mime = db.mimeTypeForFile(item->suggestedFileName(), QMimeDatabase::MatchExtension);
    }
    args.setMimeType(mime.name());

    if (objective == WebEnginePartDownloadManager::DownloadObjective::SaveUrl || objective == WebEnginePartDownloadManager::DownloadObjective::SavePageAs) {
        bArgs.setAllowedUrlActions(Konq::AllowedUrlActions{Konq::UrlAction::Save});
    } else if (objective == WebEnginePartDownloadManager::DownloadObjective::AskUser) {
        bArgs.setForceShowActionDialog(true);
    }

    if (objective == WebEnginePartDownloadManager::DownloadObjective::SavePageAs) {
        bArgs.setForcesNewWindow(true);
        job->setCalledForSaveAs(true);
        auto displayLocalFile = [this](KonqInterfaces::DownloadJob *job, const QUrl &url){
            if (job->error()) {
                return;
            }
            disconnect(job, &DownloadJob::downloadResult, m_part, &WebEnginePart::displayActOnDownloadedFileBar);
            BrowserArguments newBargs;
            newBargs.setAllowedUrlActions(Konq::AllowedUrlActions{Konq::UrlAction::Embed});
            emit m_part->browserExtension()->browserOpenUrlRequest(url, {}, newBargs);
        };
        connect(job, &DownloadJob::downloadResult, m_part, displayLocalFile);
    }

    connect(job, &DownloadJob::downloadResult, m_part, &WebEnginePart::displayActOnDownloadedFileBar);
    emit m_part->browserExtension()->browserOpenUrlRequestSync(downloadUrl, args, bArgs, true);

    if (item->state() == QWebEngineDownloadRequest::DownloadRequested) {
        qCDebug(WEBENGINEPART_LOG) << "Automatically accepting download for" << item->url() << "This shouldn't happen";
        item->accept();
    }

}

void WebEnginePage::setDropOperationStarted()
{
    m_dropOperationTimer->start(100);
}

bool WebEnginePage::askBrowserToOpenUrl(const QUrl& url, const QString& mimetype, const KParts::OpenUrlArguments &_args, const BrowserArguments &bargs)
{
    KParts::OpenUrlArguments args(_args);
    args.setMimeType(mimetype);
    args.metaData().insert("DontSendToDefaultHTMLPart", "");
    emit m_part->browserExtension()->browserOpenUrlRequest(url, args, bargs);
    return false;
}

bool WebEnginePage::shouldOpenUrl(const QUrl& url) const
{
    static const QStringList s_forcedSchemes{QStringLiteral("remote"), QStringLiteral("trash")};
    if (s_forcedSchemes.contains(url.scheme())) {
        return false;
    }
    if (!url.isLocalFile()) {
        return true;
    }

    KonqInterfaces::Browser *iface = KonqInterfaces::Browser::browser(qApp);
    if (!iface) {
        qCDebug(WEBENGINEPART_LOG) << "Konqueror application should provide KonqInterfaces::Browser interface but it doesn't";
        return true;
    }
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(url.path());
    QString partToUse = iface->partForLocalFile(url.path(), mime.name());
    return partToUse == part()->metaData().pluginId();
}

void WebEnginePage::handleNavigationRequest(QWebEngineNavigationRequest& req)
{
    const QUrl url = req.url();
    //Ask the browser for permission to navigate away. In Konqueror, if a view is locked, it can't navigate to somewhere else
    if (req.isMainFrame()) {
        KonqInterfaces::Browser *browser = KonqInterfaces::Browser::browser(qApp);
        if ((browser && !browser->canNavigateTo(part(), url)) || !shouldOpenUrl(url)) {
            askBrowserToOpenUrl(url);
            req.reject();
            return;
        }
    }

    // Handle "mailto:" url here...
    if (handleMailToUrl(url, req.navigationType())) {
        req.reject();
        return;
    }

    switch (req.navigationType()) {
        case ReqNavigationType::FormSubmittedNavigation:
            if (!checkFormData(url)) {
                req.reject();
                return;
            }
            if (part() && part()->wallet()) {
                part()->wallet()->saveFormsInPage(this);
            }
            break;
        case ReqNavigationType::BackForwardNavigation:
            // If history navigation is locked, ignore all such requests...
            if (property("HistoryNavigationLocked").toBool()) {
                setProperty("HistoryNavigationLocked", QVariant());
                qCDebug(WEBENGINEPART_LOG) << "Rejected history navigation because 'HistoryNavigationLocked' property is set!";
                req.reject();
                return;
            }
            //qCDebug(WEBENGINEPART_LOG) << "Navigating to item (" << history()->currentItemIndex()
            //         << "of" << history()->count() << "):" << history()->currentItem().url();
            break;
        case ReqNavigationType::ReloadNavigation:
//            setRequestMetaData(QL1S("cache"), QL1S("reload"));
            break;
        case ReqNavigationType::OtherNavigation:
            qCDebug(WEBENGINEPART_LOG) << "Triggered by javascript";
            break;
        default:
            break;
    }

    // Honor the enabling/disabling of plugins per host.
    settings()->setAttribute(QWebEngineSettings::PluginsEnabled, WebEngineSettings::self()->isPluginsEnabled(url.host()));

    if (req.isMainFrame()) {
        //Setting the javascript policy after the page has been loaded can be too late
        //(see bug #490321), so we also do it here
        setPageJScriptPolicy(url);
        emit mainFrameNavigationRequested(this, url);
    }
    req.accept();
}

#if 0
static int errorCodeFromReply(QNetworkReply* reply)
{
    // First check if there is a KIO error code sent back and use that,
    // if not attempt to convert QNetworkReply's NetworkError to KIO::Error.
    QVariant attr = reply->attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::KioError));
    if (attr.isValid() && attr.type() == QVariant::Int)
        return attr.toInt();

    switch (reply->error()) {
        case QNetworkReply::ConnectionRefusedError:
            return KIO::ERR_CANNOT_CONNECT;
        case QNetworkReply::HostNotFoundError:
            return KIO::ERR_UNKNOWN_HOST;
        case QNetworkReply::TimeoutError:
            return KIO::ERR_SERVER_TIMEOUT;
        case QNetworkReply::OperationCanceledError:
            return KIO::ERR_USER_CANCELED;
        case QNetworkReply::ProxyNotFoundError:
            return KIO::ERR_UNKNOWN_PROXY_HOST;
        case QNetworkReply::ContentAccessDenied:
            return KIO::ERR_ACCESS_DENIED;
        case QNetworkReply::ContentOperationNotPermittedError:
            return KIO::ERR_WRITE_ACCESS_DENIED;
        case QNetworkReply::ContentNotFoundError:
            return KIO::ERR_NO_CONTENT;
        case QNetworkReply::AuthenticationRequiredError:
            return KIO::ERR_CANNOT_AUTHENTICATE;
        case QNetworkReply::ProtocolUnknownError:
            return KIO::ERR_UNSUPPORTED_PROTOCOL;
        case QNetworkReply::ProtocolInvalidOperationError:
            return KIO::ERR_UNSUPPORTED_ACTION;
        case QNetworkReply::UnknownNetworkError:
            return KIO::ERR_UNKNOWN;
        case QNetworkReply::NoError:
        default:
            break;
    }

    return 0;
}
#endif

void WebEnginePage::handleCertificateError(const QWebEngineCertificateError &ce) {
    WebEnginePartControls::self()->handleCertificateError(ce, this);
}

WebEnginePart* WebEnginePage::part() const
{
    return m_part.data();
}

void WebEnginePage::setPart(WebEnginePart* part)
{
    m_part = part;
}

void WebEnginePage::slotLoadFinished(bool ok)
{
    QUrl requestUrl = url();
    requestUrl.setUserInfo(QString());
#if 0
    const bool shouldResetSslInfo = (m_sslInfo.isValid() && !domainSchemeMatch(requestUrl, m_sslInfo.url()));
    QWebFrame* frame = qobject_cast<QWebFrame *>(reply->request().originatingObject());
    if (!frame)
        return;
    const bool isMainFrameRequest = (frame == mainFrame());
#else
    // PORTING_TODO
    const bool isMainFrameRequest = true;
#endif

#if 0
    // Only deal with non-redirect responses...
    const QVariant redirectVar = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);

    if (isMainFrameRequest && redirectVar.isValid()) {
        m_sslInfo.restoreFrom(reply->attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::MetaData)),
                              reply->url(), shouldResetSslInfo);
        return;
    }

    const int errCode = errorCodeFromReply(reply);
    qCDebug(WEBENGINEPART_LOG) << frame << "is main frame request?" << isMainFrameRequest << requestUrl;
#endif

    if (ok) {
        if (isMainFrameRequest) {
#if 0
            m_sslInfo.restoreFrom(reply->attribute(static_cast<QNetworkRequest::Attribute>(KIO::AccessManager::MetaData)),
                    reply->url(), shouldResetSslInfo);
#endif
            setPageJScriptPolicy(url());
        }
    } else {
    // Handle any error...
#if 0
    switch (errCode) {
        case 0:
        case KIO::ERR_NO_CONTENT:
            break;
        case KIO::ERR_ABORTED:
        case KIO::ERR_USER_CANCELED: // Do nothing if request is cancelled/aborted
            //qCDebug(WEBENGINEPART_LOG) << "User aborted request!";
            m_ignoreError = true;
            emit loadAborted(QUrl());
            return;
        // Handle the user clicking on a link that refers to a directory
        // Since KIO cannot automatically convert a GET request to a LISTDIR one.
        case KIO::ERR_IS_DIRECTORY:
            m_ignoreError = true;
            emit loadAborted(reply->url());
            return;
        default:
            // Make sure the saveFrameStateRequested signal is emitted so
            // the page can restored properly.
            if (isMainFrameRequest)
                emit saveFrameStateRequested(frame, 0);

            m_ignoreError = (reply->attribute(QNetworkRequest::User).toInt() == QNetworkReply::ContentAccessDenied);
            m_kioErrorCode = errCode;
            break;
#endif
    }

    if (isMainFrameRequest) {
        const WebEnginePageSecurity security = (m_sslInfo.isValid() ? PageEncrypted : PageUnencrypted);
        emit m_part->navigationExtension()->setPageSecurity(security);
    }
}

void WebEnginePage::slotFeaturePermissionRequested(const QUrl& url, QWebEnginePage::Feature feature)
{
    //url.path() is always / (meaning that permissions should be granted site-wide and not per page)
    QUrl thisUrl(this->url());
    thisUrl.setPath("/");
    thisUrl.setQuery(QString());
    thisUrl.setFragment(QString());
    if (url == thisUrl) {
        part()->slotShowFeaturePermissionBar(url, feature);
        return;
    }
    switch(feature) {
    case QWebEnginePage::Notifications:
        // FIXME: We should have a setting to tell if this is enabled, but so far it is always enabled.
        setFeaturePermission(url, feature, QWebEnginePage::PermissionGrantedByUser);
        break;
    case QWebEnginePage::Geolocation:
        if (KMessageBox::warningContinueCancel(nullptr, i18n("This site is attempting to "
                                                       "access information about your "
                                                       "physical location.\n"
                                                       "Do you want to allow it access?"),
                                            i18n("Network Transmission"),
                                            KGuiItem(i18n("Allow access")),
                                            KStandardGuiItem::cancel(),
                                            QStringLiteral("WarnGeolocation")) == KMessageBox::Cancel) {
            setFeaturePermission(url, feature, QWebEnginePage::PermissionDeniedByUser);
        } else {
            setFeaturePermission(url, feature, QWebEnginePage::PermissionGrantedByUser);
        }
        break;
    default:
        setFeaturePermission(url, feature, QWebEnginePage::PermissionUnknown);
        break;
    }
}

void WebEnginePage::slotGeometryChangeRequested(const QRect & rect)
{
    const QString host = url().host();

    // NOTE: If a new window was created from another window which is in
    // maximized mode and its width and/or height were not specified at the
    // time of its creation, which is always the case in QWebEnginePage::createWindow,
    // then any move operation will seem not to work. That is because the new
    // window will be in maximized mode where moving it will not be possible...
    if (WebEngineSettings::self()->windowMovePolicy(host) == HtmlSettingsInterface::JSWindowMoveAllow &&
        (view()->x() != rect.x() || view()->y() != rect.y()))
        emit m_part->navigationExtension()->moveTopLevelWidget(rect.x(), rect.y());

    const int height = rect.height();
    const int width = rect.width();

    // parts of following code are based on kjs_window.cpp
    // Security check: within desktop limits and bigger than 100x100 (per spec)
    if (width < 100 || height < 100) {
        qCWarning(WEBENGINEPART_LOG) << "Window resize refused, window would be too small (" << width << "," << height << ")";
        return;
    }

    QRect sg = view()->screen()->virtualGeometry();

    if (width > sg.width() || height > sg.height()) {
        qCWarning(WEBENGINEPART_LOG) << "Window resize refused, window would be too big (" << width << "," << height << ")";
        return;
    }

    if (WebEngineSettings::self()->windowResizePolicy(host) == HtmlSettingsInterface::JSWindowResizeAllow) {
        //qCDebug(WEBENGINEPART_LOG) << "resizing to " << width << "x" << height;
        emit m_part->navigationExtension()->resizeTopLevelWidget(width, height);
    }

    // If the window is out of the desktop, move it up/left
    // (maybe we should use workarea instead of sg, otherwise the window ends up below kicker)
    const int right = view()->x() + view()->frameGeometry().width();
    const int bottom = view()->y() + view()->frameGeometry().height();
    int moveByX = 0, moveByY = 0;
    if (right > sg.right())
        moveByX = - right + sg.right(); // always <0
    if (bottom > sg.bottom())
        moveByY = - bottom + sg.bottom(); // always <0

    if ((moveByX || moveByY) && WebEngineSettings::self()->windowMovePolicy(host) == HtmlSettingsInterface::JSWindowMoveAllow)
        emit m_part->navigationExtension()->moveTopLevelWidget(view()->x() + moveByX, view()->y() + moveByY);
}

bool WebEnginePage::checkFormData(const QUrl &url) const
{
    const QString scheme (url.scheme());

    if (m_sslInfo.isValid() &&
        !scheme.compare(QL1S("https")) && !scheme.compare(QL1S("mailto")) &&
        (KMessageBox::warningContinueCancel(nullptr,
                                           i18n("Warning: This is a secure form "
                                                "but it is attempting to send "
                                                "your data back unencrypted.\n"
                                                "A third party may be able to "
                                                "intercept and view this "
                                                "information.\nAre you sure you "
                                                "want to send the data unencrypted?"),
                                           i18n("Network Transmission"),
                                           KGuiItem(i18n("&Send Unencrypted")))  == KMessageBox::Cancel)) {

        return false;
    }


    if (scheme.compare(QL1S("mailto")) == 0 &&
        (KMessageBox::warningContinueCancel(nullptr, i18n("This site is attempting to "
                                                    "submit form data via email.\n"
                                                    "Do you want to continue?"),
                                            i18n("Network Transmission"),
                                            KGuiItem(i18n("&Send Email")),
                                            KStandardGuiItem::cancel(),
                                            QStringLiteral("WarnTriedEmailSubmit")) == KMessageBox::Cancel)) {
        return false;
    }

    return true;
}

// Sanitizes the "mailto:" url, e.g. strips out any "attach" parameters.
static QUrl sanitizeMailToUrl(const QUrl &url, QStringList& files) {
    QUrl sanitizedUrl;

    // NOTE: This is necessary to ensure we can properly use QUrl's query
    // related APIs to process 'mailto:' urls of form 'mailto:foo@bar.com'.
    if (url.hasQuery()) {
        sanitizedUrl = url;
    }
    else {
        sanitizedUrl = QUrl(url.scheme() + QL1S(":?") + url.path());
    }

    QUrlQuery query(sanitizedUrl);
    const QList<QPair<QString, QString> > items (query.queryItems());

    QUrlQuery sanitizedQuery;
    for(auto queryItem : items) {
        if (queryItem.first.contains(QL1C('@')) && queryItem.second.isEmpty()) {
            // ### DF: this hack breaks mailto:faure@kde.org, kmail doesn't expect mailto:?to=faure@kde.org
            queryItem.second = queryItem.first;
            queryItem.first = QStringLiteral("to");
        } else if (QString::compare(queryItem.first, QL1S("attach"), Qt::CaseInsensitive) == 0) {
            files << queryItem.second;
            continue;
        }
        sanitizedQuery.addQueryItem(queryItem.first, queryItem.second);
    }

    sanitizedUrl.setQuery(sanitizedQuery);
    return sanitizedUrl;
}

bool WebEnginePage::handleMailToUrl (const QUrl &url, QWebEngineNavigationRequest::NavigationType type) const
{
    if (url.scheme() == QL1S("mailto")) {
        QStringList files;
        QUrl mailtoUrl (sanitizeMailToUrl(url, files));

        switch (type) {
            case ReqNavigationType::LinkClickedNavigation:
                if (!files.isEmpty() && KMessageBox::warningContinueCancelList(nullptr,
                                                                               i18n("<qt>Do you want to allow this site to attach "
                                                                                    "the following files to the email message?</qt>"),
                                                                               files, i18n("Email Attachment Confirmation"),
                                                                               KGuiItem(i18n("&Allow attachments")),
                                                                               KGuiItem(i18n("&Ignore attachments")), QL1S("WarnEmailAttachment")) == KMessageBox::Continue) {

                   // Re-add the attachments...
                    QStringListIterator filesIt (files);
                    QUrlQuery query(mailtoUrl);
                    while (filesIt.hasNext()) {
                        query.addQueryItem(QL1S("attach"), filesIt.next());
                    }
                    mailtoUrl.setQuery(query);
                }
                break;
            case ReqNavigationType::FormSubmittedNavigation:
                if (!files.isEmpty()) {
                    KMessageBox::information(nullptr, i18n("This site attempted to attach a file from your "
                                                     "computer in the form submission. The attachment "
                                                     "was removed for your protection."),
                                             i18n("Attachment Removed"), QStringLiteral("InfoTriedAttach"));
                }
                break;
            default:
                 break;
        }

        //qCDebug(WEBENGINEPART_LOG) << "Emitting openUrlRequest with " << mailtoUrl;
        emit m_part->browserExtension()->browserOpenUrlRequest(mailtoUrl);
        return true;
    }

    return false;
}

void WebEnginePage::setPageJScriptPolicy(const QUrl &url)
{
    const QString hostname (url.host());
    settings()->setAttribute(QWebEngineSettings::JavascriptEnabled,
                             WebEngineSettings::self()->isJavaScriptEnabled(hostname));

    const HtmlSettingsInterface::JSWindowOpenPolicy policy = WebEngineSettings::self()->windowOpenPolicy(hostname);
    settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows,
                             (policy != HtmlSettingsInterface::JSWindowOpenDeny &&
                              policy != HtmlSettingsInterface::JSWindowOpenSmart));
}

void WebEnginePage::slotAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth)
{
    KIO::AuthInfo info;
    info.url = requestUrl;
    info.username = auth->user();
    info.realmValue = auth->realm();
    // If no realm metadata, then make sure path matching is turned on.
    info.verifyPath = info.realmValue.isEmpty();

    const QString errorMsg = QString();
    const int ret = m_passwdServerClient->queryAuthInfo(&info, errorMsg, view()->window()->winId(), KUserTimestamp::userTimestamp());
    if (ret == KJob::NoError) {
        auth->setUser(info.username);
        auth->setPassword(info.password);
    } else {
        // Set authenticator null if dialog is cancelled
        // or if we couldn't communicate with kpasswdserver
        *auth = QAuthenticator();
    }
}

void WebEnginePage::changeFullScreenMode(QWebEngineFullScreenRequest req)
{
        BrowserInterface *iface = part()->browserExtension()->browserInterface();
        if (iface) {
            req.accept();
            iface->callMethod("toggleCompleteFullScreen", req.toggleOn());
        } else {
            req.reject();
        }
}


void WebEnginePage::setStatusBarText(const QString& text)
{
    if (m_part) {
        emit m_part->setStatusBarText(text);
    }
}

void WebEnginePage::changeLifecycleState(QWebEnginePage::LifecycleState recommendedState)
{
    using State = QWebEnginePage::LifecycleState;

    State newState = State::Active;
    bool loading = m_loadStatus == QWebEngineLoadingInfo::LoadStartedStatus;
    if (recommendedState != State::Active && !isVisible() && !loading && controls()->isPageLifecycleManagementEnabled()) {
        newState = State::Frozen;
    }
    setLifecycleState(newState);
}

void WebEnginePage::updateUserStyleSheet(const QString& script)
{
    runJavaScript(script, QWebEngineScript::ApplicationWorld);
}

#ifdef MEDIAREQUEST_SUPPORTED
void WebEnginePage::chooseDesktopMedia(const QWebEngineDesktopMediaRequest& request)
{
    CaptureSourceChooserDlg chooser(url(), request.windowsModel(), request.screensModel(), view());
    chooser.exec();
    QModelIndex chosenIndex = chooser.choice();
    if (chosenIndex.model() == request.windowsModel()) {
        request.selectWindow(chosenIndex);
    } else if (chosenIndex.model() == request.screensModel()) {
        request.selectScreen(chosenIndex);
    } else {
        request.cancel();
        return;
    }
}
#endif

void WebEnginePage::print()
{
    m_part->displayPrintPreview();
}

#ifdef WEBENGINE_FRAMES_SUPPORTED
void WebEnginePage::printFrame(QWebEngineFrame frame)
{
    QString baseName = frame.htmlName();
    if (baseName.isEmpty()) {
        baseName = frame.name();
    }
    if (baseName.isEmpty()) {
        baseName = QString::number(QTime::currentTime().msecsSinceStartOfDay());
    }
    baseName.append(QLatin1StringView(".pdf"));
    QString fileName = Konq::generateUniqueFileName(baseName, m_part->temporaryDir().path());
    auto callback = [this, fileName](const QByteArray &data) {
        if (data.isEmpty()) {
            KMessageBox::error(view(), i18n("An error occurred while converting the page to PDF"));
            return;
        }
        QFile file(fileName);
        if (!file.open(QFile::WriteOnly)) {
            KMessageBox::error(view(), i18n("An error occurred while saving a PDF version of the page as %1", fileName));
            return;
        }
        file.write(data);
        file.close();

        BrowserArguments ba;
        ba.setNewTab(true);
        ba.setForcesNewWindow(true);
        emit m_part->browserExtension()->browserOpenUrlRequest(QUrl::fromLocalFile(fileName), {}, ba, true);
    };
    frame.printToPdf(callback);
}
#endif

void WebEnginePage::assumingNewWindowRequestsAreUserInitiated(const std::function<void ()>& fnc)
{
    bool old = m_assumeNewWindowRequestsUserInitiated;
    m_assumeNewWindowRequestsUserInitiated = true;
    fnc();
    m_assumeNewWindowRequestsUserInitiated = old;
}

void WebEnginePage::createNewWindow(QWebEngineNewWindowRequest& req)
{
    if (m_dropOperationTimer->isActive()) {
        m_dropOperationTimer->stop();
        req.openIn(this);
        return;
    }

    const bool requiresNewTab = req.destination() == QWebEngineNewWindowRequest::InNewBackgroundTab || req.destination() == QWebEngineNewWindowRequest::InNewTab;
    if (req.isUserInitiated() && !requiresNewTab) {
        if (!decideHandlingOfJavascripWindow(req.requestedUrl())) {
            return;
        }
    }
    BrowserArguments bargs;
    bargs.setEmbedWith(part()->metaData().pluginId());
    bargs.setAllowedUrlActions({Konq::UrlAction::Embed});

    //Don't set forcesNewWindow for if req.destination() is InNewDialog because it include popups, which the user may want to open in a new tab
    if (req.destination() == QWebEngineNewWindowRequest::InNewWindow) {
        bargs.setForcesNewWindow(true);
    }

    KParts::OpenUrlArguments args;
    args.setMimeType(QL1S("text/html"));
    args.setActionRequestedByUser(m_assumeNewWindowRequestsUserInitiated || req.isUserInitiated());

    WindowArgs wargs;
    wargs.setX(req.requestedGeometry().x());
    wargs.setY(req.requestedGeometry().y());
    wargs.setWidth(req.requestedGeometry().width());
    wargs.setHeight(req.requestedGeometry().height());
    if (req.destination() == QWebEngineNewWindowRequest::InNewBackgroundTab) {
        wargs.setLowerWindow(true);
    }

    KParts::ReadOnlyPart* newWindowPart = nullptr;
    emit part()->browserExtension()->browserCreateNewWindow(req.requestedUrl(), args, bargs, wargs, &newWindowPart);
    WebEnginePart *newWebEnginePart = qobject_cast<WebEnginePart*>(newWindowPart);
    if (newWebEnginePart) {
        req.openIn(newWebEnginePart->page());
    }
}

bool WebEnginePage::decideHandlingOfJavascripWindow(const QUrl url) const
{
    const HtmlSettingsInterface::JSWindowOpenPolicy policy = WebEngineSettings::self()->windowOpenPolicy(url.host());
    switch (policy) {
    case HtmlSettingsInterface::JSWindowOpenDeny:
        // TODO: Implement support for dealing with blocked pop up windows.
        return false;
    case HtmlSettingsInterface::JSWindowOpenAsk: {
        const QString message = (url.isEmpty() ?
                                    i18n("This site is requesting to open a new popup window.\n"
                                        "Do you want to allow this?") :
                                    i18n("<qt>This site is requesting to open a popup window to"
                                        "<p>%1</p><br/>Do you want to allow this?</qt>",
                                        KStringHandler::rsqueeze(url.toDisplayString().toHtmlEscaped(), 100)));
        return KMessageBox::questionTwoActions(view(), message, i18n("Javascript Popup Confirmation"),
                                        KGuiItem(i18n("Allow")), KGuiItem(i18n("Do Not Allow"))) == KMessageBox::PrimaryAction;
        // TODO: Implement support for dealing with blocked pop up windows.
    }
    default:
        break;
    }
    return true;
}
