File indexing completed on 2024-05-19 05:01:22

0001 /*
0002     This file is part of the KDE project.
0003 
0004     SPDX-FileCopyrightText: 2009 Dawit Alemayehu <adawit@kde.org>
0005 
0006     The code for the function slotPrintPreview was adapted from the PrintMe Qt example
0007     and is distributed under the BSD license:
0008     SPDX-License-Identifier: LGPL-2.1-or-later AND BSD-3-Clause
0009 */
0010 
0011 #include "webenginepart_ext.h"
0012 
0013 #include "webenginepart.h"
0014 #include "webengineview.h"
0015 #include "webenginepage.h"
0016 #include "settings/webenginesettings.h"
0017 #include <webenginepart_debug.h>
0018 #include "webenginepage.h"
0019 #include "webenginepartcontrols.h"
0020 #include "webenginepartdownloadmanager.h"
0021 
0022 #include "libkonq_utils.h"
0023 
0024 #include <QWebEngineSettings>
0025 
0026 #include <KDesktopFile>
0027 #include <KConfigGroup>
0028 #include <KSharedConfig>
0029 #include <KProtocolInfo>
0030 #include <QInputDialog>
0031 #include <KLocalizedString>
0032 #include <QTemporaryFile>
0033 #include <KUriFilter>
0034 #include <Sonnet/Dialog>
0035 #include <sonnet/backgroundchecker.h>
0036 #include <KIO/JobUiDelegate>
0037 #include <KIO/OpenUrlJob>
0038 #include <KEMailClientLauncherJob>
0039 #include <KIO/JobUiDelegateFactory>
0040 
0041 #include <QBuffer>
0042 #include <QVariant>
0043 #include <QClipboard>
0044 #include <QApplication>
0045 #include <QAction>
0046 #include <QPrinter>
0047 #include <QPrintDialog>
0048 #include <QPrintPreviewDialog>
0049 #include <QWebEngineHistory>
0050 #include <QMimeData>
0051 #include <QPrinterInfo>
0052 #include <QJsonDocument>
0053 #include <QJsonArray>
0054 #include <QStringView>
0055 
0056 #define QL1S(x)     QLatin1String(x)
0057 #define QL1C(x)     QLatin1Char(x)
0058 
0059 using Element = AsyncSelectorInterface::Element;
0060 
0061 using namespace KonqInterfaces;
0062 
0063 // A functor that calls a member function
0064 template<typename Arg, typename R, typename C>
0065 struct InvokeWrapper {
0066     R *receiver;
0067     void (C::*memberFun)(Arg);
0068     void operator()(Arg result)
0069     {
0070         (receiver->*memberFun)(result);
0071     }
0072 };
0073 
0074 template<typename Arg, typename R, typename C>
0075 InvokeWrapper<Arg, R, C> invoke(R *receiver, void (C::*memberFun)(Arg))
0076 {
0077     return InvokeWrapper<Arg, R, C>{receiver, memberFun};
0078 }
0079 
0080 WebEngineNavigationExtension::WebEngineNavigationExtension(WebEnginePart *parent, const QByteArray& cachedHistoryData)
0081                        : BrowserExtension(parent),
0082                         m_part(parent),
0083                         mCurrentPrinter(nullptr)
0084 {
0085     emit enableAction("cut", false);
0086     emit enableAction("copy", false);
0087     emit enableAction("paste", false);
0088     emit enableAction("print", true);
0089 
0090 #if QT_VERSION_MAJOR >= 6
0091     connect(view(), &QWebEngineView::printFinished, this, &WebEngineNavigationExtension::slotHandlePagePrinted);
0092 #endif
0093 
0094     if (cachedHistoryData.isEmpty()) {
0095         return;
0096     }
0097 
0098     QBuffer buffer;
0099     buffer.setData(cachedHistoryData);
0100     if (!buffer.open(QIODevice::ReadOnly)) {
0101         return;
0102     }
0103 
0104     // NOTE: When restoring history, webengine automatically navigates to
0105     // the previous "currentItem". Since we do not want that to happen,
0106     // we set a property on the WebEnginePage object that is used to allow or
0107     // disallow history navigation in WebEnginePage::acceptNavigationRequest.
0108     view()->page()->setProperty("HistoryNavigationLocked", true);
0109     QDataStream s (&buffer);
0110     s >> *(view()->history());
0111 }
0112 
0113 WebEngineNavigationExtension::~WebEngineNavigationExtension()
0114 {
0115 }
0116 
0117 WebEngineView* WebEngineNavigationExtension::view()
0118 {
0119     if (!m_view && m_part) {
0120         m_view = qobject_cast<WebEngineView*>(m_part->view());
0121     }
0122 
0123     return m_view;
0124 }
0125 
0126 WebEnginePage* WebEngineNavigationExtension::page()
0127 {
0128     WebEngineView *v = view();
0129     if (v) {
0130         return qobject_cast<WebEnginePage*>(v->page());
0131     } else {
0132         return nullptr;
0133     }
0134 }
0135 
0136 int WebEngineNavigationExtension::xOffset()
0137 {
0138     if (view()) {
0139         return view()->page()->scrollPosition().x();
0140     }
0141 
0142     return KParts::NavigationExtension::xOffset();
0143 }
0144 
0145 int WebEngineNavigationExtension::yOffset()
0146 {
0147    if (view()) {
0148         return view()->page()->scrollPosition().y();
0149    }
0150 
0151     return KParts::NavigationExtension::yOffset();
0152 }
0153 
0154 void WebEngineNavigationExtension::saveState(QDataStream &stream)
0155 {
0156     // TODO: Save information such as form data from the current page.
0157     QWebEngineHistory* history = (view() ? view()->history() : nullptr);
0158     const int historyIndex = (history ? history->currentItemIndex() : -1);
0159     const QUrl historyUrl = (history && historyIndex > -1) ? QUrl(history->currentItem().url()) : m_part->url();
0160 
0161     stream << historyUrl
0162            << static_cast<qint32>(xOffset())
0163            << static_cast<qint32>(yOffset())
0164            << historyIndex
0165            << m_historyData;
0166 }
0167 
0168 void WebEngineNavigationExtension::restoreState(QDataStream &stream)
0169 {
0170     QUrl u;
0171     QByteArray historyData;
0172     qint32 xOfs = -1, yOfs = -1, historyItemIndex = -1;
0173     //TODO KF6: it seems that for some reason historyData is empty, at least when restoring a saved session
0174     stream >> u >> xOfs >> yOfs >> historyItemIndex >> historyData;
0175 
0176     QWebEngineHistory* history = (view() ? view()->page()->history() : nullptr);
0177     if (history) {
0178         bool success = false;
0179         if (history->count() == 0) {   // Handle restoration: crash recovery, tab close undo, session restore
0180             if (!historyData.isEmpty()) {
0181                 historyData = qUncompress(historyData); // uncompress the history data...
0182                 QBuffer buffer (&historyData);
0183                 if (buffer.open(QIODevice::ReadOnly)) {
0184                     QDataStream stream (&buffer);
0185                     view()->page()->setProperty("HistoryNavigationLocked", true);
0186                     stream >> *history;
0187                     QWebEngineHistoryItem currentItem(history->currentItem());
0188                     if (currentItem.isValid()) {
0189                         if (currentItem.isValid() && (xOfs != -1 || yOfs != -1)) {
0190                             const QPoint scrollPos (xOfs, yOfs);
0191 //                            currentItem.setUserData(scrollPos);
0192                         }
0193                         // NOTE 1: The following Konqueror specific workaround is necessary
0194                         // because Konqueror only preserves information for the last visited
0195                         // page. However, we save the entire history content in saveState and
0196                         // and hence need to eliminate all but the current item here.
0197                         // NOTE 2: This condition only applies when Konqueror is restored from
0198                         // abnormal termination ; a crash and/or a session restoration.
0199                         if (QCoreApplication::applicationName() == QLatin1String("konqueror")) {
0200                             history->clear();
0201                         }
0202                         //qCDebug(WEBENGINEPART_LOG) << "Restoring URL:" << currentItem.url();
0203                         m_part->setProperty("NoEmitOpenUrlNotification", true);
0204                         history->goToItem(currentItem);
0205                     }
0206                 }
0207             }
0208             success = (history->count() > 0);
0209         } else {        // Handle navigation: back and forward button navigation.
0210             //qCDebug(WEBENGINEPART_LOG) << "history count:" << history->count() << "request index:" << historyItemIndex;
0211             if (history->count() > historyItemIndex && historyItemIndex > -1) {
0212                 QWebEngineHistoryItem item (history->itemAt(historyItemIndex));
0213                 //qCDebug(WEBENGINEPART_LOG) << "URL:" << u << "Item URL:" << item.url();
0214                 if (u == item.url()) {
0215                     if (item.isValid() && (xOfs != -1 || yOfs != -1)) {
0216                         const QPoint scrollPos (xOfs, yOfs);
0217 //                        item.setUserData(scrollPos);
0218                     }
0219                     m_part->setProperty("NoEmitOpenUrlNotification", true);
0220                     history->goToItem(item);
0221                     success = true;
0222                 }
0223             }
0224         }
0225 
0226         if (success) {
0227             return;
0228         }
0229     }
0230 
0231     // As a last resort, in case the history restoration logic above fails,
0232     // attempt to open the requested URL directly.
0233     qCDebug(WEBENGINEPART_LOG) << "Normal history navigation logic failed! Falling back to opening url directly.";
0234     m_part->openUrl(u);
0235 }
0236 
0237 
0238 void WebEngineNavigationExtension::cut()
0239 {
0240     if (view())
0241         view()->triggerPageAction(QWebEnginePage::Cut);
0242 }
0243 
0244 void WebEngineNavigationExtension::copy()
0245 {
0246     if (view())
0247         view()->triggerPageAction(QWebEnginePage::Copy);
0248 }
0249 
0250 void WebEngineNavigationExtension::paste()
0251 {
0252     if (view())
0253         view()->triggerPageAction(QWebEnginePage::Paste);
0254 }
0255 
0256 void WebEngineNavigationExtension::slotSaveDocument()
0257 {
0258     WebEnginePage *pg = page();
0259     if (pg) {
0260         WebEnginePartControls::self()->downloadManager()->specifyDownloadObjective(pg->url(), pg,
0261             WebEnginePartDownloadManager::DownloadObjective::SaveAs);
0262         pg->download(pg->url());
0263     }
0264 }
0265 
0266 void WebEngineNavigationExtension::slotSaveFullHTMLPage()
0267 {
0268     WebEnginePage *p = page();
0269     if (p) {
0270         p->triggerAction(QWebEnginePage::SavePage);
0271     }
0272 }
0273 
0274 void WebEngineNavigationExtension::print()
0275 {
0276     if (view()) {
0277         mCurrentPrinter = new QPrinter();
0278         QPointer<QPrintDialog> dialog = new QPrintDialog(mCurrentPrinter, nullptr);
0279         dialog->setWindowTitle(i18n("Print Document"));
0280         if (dialog->exec() != QDialog::Accepted) {
0281             slotHandlePagePrinted(false);
0282             delete dialog;
0283             return;
0284         }
0285         delete dialog;
0286 #if QT_VERSION_MAJOR < 6
0287         view()->page()->print(mCurrentPrinter, invoke(this, &WebEngineNavigationExtension::slotHandlePagePrinted));
0288 #else
0289         view()->print(mCurrentPrinter);
0290 #endif
0291     }
0292 }
0293 
0294 void WebEngineNavigationExtension::slotHandlePagePrinted(bool result)
0295 {
0296     Q_UNUSED(result);
0297     delete mCurrentPrinter;
0298     mCurrentPrinter = nullptr;
0299 }
0300 
0301 void WebEngineNavigationExtension::updateEditActions()
0302 {
0303     if (!view())
0304         return;
0305 
0306     emit enableAction("cut", view()->pageAction(QWebEnginePage::Cut)->isEnabled());
0307     emit enableAction("copy", view()->pageAction(QWebEnginePage::Copy)->isEnabled());
0308     emit enableAction("paste", view()->pageAction(QWebEnginePage::Paste)->isEnabled());
0309 }
0310 
0311 void WebEngineNavigationExtension::updateActions()
0312 {
0313     const QString protocol (m_part->url().scheme());
0314     const bool isValidDocument = (protocol != QL1S("about") && protocol != QL1S("error") && protocol != QL1S("konq"));
0315     emit enableAction("print", isValidDocument);
0316 }
0317 
0318 void WebEngineNavigationExtension::searchProvider()
0319 {
0320     if (!view())
0321         return;
0322 
0323     QAction *action = qobject_cast<QAction*>(sender());
0324     if (!action)
0325         return;
0326 
0327     QUrl url = action->data().toUrl();
0328 
0329     if (url.host().isEmpty()) {
0330         KUriFilterData data;
0331         data.setData(action->data().toString());
0332         if (KUriFilter::self()->filterSearchUri(data, KUriFilter::WebShortcutFilter))
0333             url = data.uri();
0334     }
0335 
0336     if (!url.isValid())
0337       return;
0338 
0339     BrowserArguments bargs;
0340     bargs.frameName = QL1S("_blank");
0341 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0342     emit openUrlRequest(url, KParts::OpenUrlArguments(), bargs);
0343 #else
0344     emit browserOpenUrlRequest(url, KParts::OpenUrlArguments(), bargs);
0345 #endif
0346 }
0347 
0348 void WebEngineNavigationExtension::reparseConfiguration()
0349 {
0350     // Force the configuration stuff to reparse...
0351     WebEngineSettings::self()->init();
0352 }
0353 
0354 void WebEngineNavigationExtension::disableScrolling()
0355 {
0356     QWebEngineView* currentView = view();
0357     QWebEnginePage* page = currentView ? currentView->page() : nullptr;
0358 
0359     if (!page)
0360         return;
0361 
0362     page->runJavaScript(QStringLiteral("document.documentElement.style.overflow = 'hidden';"));
0363 }
0364 
0365 void WebEngineNavigationExtension::zoomIn()
0366 {
0367     if (view())
0368         view()->setZoomFactor(view()->zoomFactor() + 0.1);
0369 }
0370 
0371 void WebEngineNavigationExtension::zoomOut()
0372 {
0373     if (view())
0374         view()->setZoomFactor(view()->zoomFactor() - 0.1);
0375 }
0376 
0377 void WebEngineNavigationExtension::zoomNormal()
0378 {
0379     if (view()) {
0380         if (WebEngineSettings::self()->zoomToDPI())
0381             view()->setZoomFactor(view()->logicalDpiY() / 96.0f);
0382         else
0383             view()->setZoomFactor(1);
0384     }
0385 }
0386 
0387 void WebEngineNavigationExtension::toogleZoomTextOnly()
0388 {
0389     if (!view())
0390         return;
0391 
0392     KConfigGroup cgHtml(KSharedConfig::openConfig(), "HTML Settings");
0393     bool zoomTextOnly = cgHtml.readEntry( "ZoomTextOnly", false );
0394     cgHtml.writeEntry("ZoomTextOnly", !zoomTextOnly);
0395     cgHtml.sync();
0396 
0397     // view()->settings()->setAttribute(QWebEngineSettings::ZoomTextOnly, !zoomTextOnly);
0398 }
0399 
0400 void WebEngineNavigationExtension::toogleZoomToDPI()
0401 {
0402     if (!view())
0403         return;
0404 
0405     bool zoomToDPI = !WebEngineSettings::self()->zoomToDPI();
0406     WebEngineSettings::self()->setZoomToDPI(zoomToDPI);
0407     
0408     if (zoomToDPI)
0409         view()->setZoomFactor(view()->zoomFactor() * view()->logicalDpiY() / 96.0f);
0410     else
0411         view()->setZoomFactor(view()->zoomFactor() * 96.0f / view()->logicalDpiY());
0412     
0413     // Recompute default font-sizes since they are only DPI dependent when zoomToDPI is false.
0414     WebEngineSettings::self()->computeFontSizes(view()->logicalDpiY());
0415 }
0416 
0417 void WebEngineNavigationExtension::slotSelectAll()
0418 {
0419     if (view())
0420         view()->triggerPageAction(QWebEnginePage::SelectAll);
0421 }
0422 
0423 void WebEngineNavigationExtension::slotSaveImageAs()
0424 {
0425     if (view())
0426         view()->triggerPageAction(QWebEnginePage::DownloadImageToDisk);
0427 }
0428 
0429 void WebEngineNavigationExtension::slotSendImage()
0430 {
0431     if (!view()) {
0432         return;
0433     }
0434 
0435     QList<QUrl> urls = {view()->contextMenuResult()->mediaUrl()};
0436     const QString subject = view()->contextMenuResult()->mediaUrl().path();
0437 
0438     auto *job = new KEMailClientLauncherJob;
0439     job->setSubject(subject);
0440     job->setAttachments(urls);
0441     job->start();
0442 }
0443 
0444 void WebEngineNavigationExtension::slotCopyImageURL()
0445 {
0446     if (!view()) {
0447         return;
0448     }
0449 
0450     QUrl safeURL = view()->contextMenuResult()->mediaUrl();
0451     safeURL.setPassword(QString());
0452     // Set it in both the mouse selection and in the clipboard
0453     QMimeData* mimeData = new QMimeData;
0454 //TODO: Porting: test
0455     QList<QUrl> safeURLList;
0456     safeURLList.append(safeURL);
0457     mimeData->setUrls(safeURLList);
0458     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
0459 
0460     mimeData = new QMimeData;
0461     mimeData->setUrls(safeURLList);
0462     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
0463 }
0464 
0465 
0466 void WebEngineNavigationExtension::slotCopyImage()
0467 {
0468     if (!view()) {
0469         return;
0470     }
0471 
0472     QUrl safeURL; //(view()->contextMenuResult()->imageUrl());
0473     safeURL.setPassword(QString());
0474 
0475     // Set it in both the mouse selection and in the clipboard
0476     QMimeData* mimeData = new QMimeData;
0477 //    mimeData->setImageData(view()->contextMenuResult()->pixmap());
0478 //TODO: Porting: test
0479     QList<QUrl> safeURLList;
0480     safeURLList.append(safeURL);
0481     mimeData->setUrls(safeURLList);
0482     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
0483 
0484     mimeData = new QMimeData;
0485 //    mimeData->setImageData(view()->contextMenuResult()->pixmap());
0486     mimeData->setUrls(safeURLList);
0487     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
0488 }
0489 
0490 void WebEngineNavigationExtension::slotViewImage()
0491 {
0492     if (view()) {
0493         emit createNewWindow(view()->contextMenuResult()->mediaUrl());
0494     }
0495 }
0496 
0497 void WebEngineNavigationExtension::slotBlockImage()
0498 {
0499     if (!view()) {
0500         return;
0501     }
0502 
0503     bool ok = false;
0504     const QString url = QInputDialog::getText(view(), i18n("Add URL to Filter"),
0505                                               i18n("Enter the URL:"), QLineEdit::Normal,
0506                                               view()->contextMenuResult()->mediaUrl().toString(),
0507                                               &ok);
0508     if (ok) {
0509         WebEngineSettings::self()->addAdFilter(url);
0510         reparseConfiguration();
0511     }
0512 }
0513 
0514 void WebEngineNavigationExtension::slotBlockHost()
0515 {
0516     if (!view())
0517         return;
0518 
0519     QUrl url; // (view()->contextMenuResult()->imageUrl());
0520     url.setPath(QL1S("/*"));
0521     WebEngineSettings::self()->addAdFilter(url.toString(QUrl::RemoveUserInfo | QUrl::RemovePort));
0522     reparseConfiguration();
0523 }
0524 
0525 void WebEngineNavigationExtension::slotCopyLinkURL()
0526 {
0527     if (view())
0528         view()->triggerPageAction(QWebEnginePage::CopyLinkToClipboard);
0529 }
0530 
0531 void WebEngineNavigationExtension::slotCopyLinkText()
0532 {
0533     if (view()) {
0534         QMimeData* data = new QMimeData;
0535         data->setText(view()->contextMenuResult()->linkText());
0536         QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
0537     }
0538 }
0539 
0540 void WebEngineNavigationExtension::slotCopyEmailAddress()
0541 {
0542     if (view()) {
0543         QMimeData* data = new QMimeData;
0544         const QUrl url(view()->contextMenuResult()->linkUrl());
0545         data->setText(url.path());
0546         QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard);
0547     }
0548 }
0549 
0550 void WebEngineNavigationExtension::slotSaveLinkAs(const QUrl &url)
0551 {
0552     if (!view()) {
0553         return;
0554     }
0555     if (!url.isEmpty()) {
0556         WebEnginePage *pg = qobject_cast<WebEnginePage*>(view()->page());
0557         if (pg) {
0558             WebEnginePartControls::self()->downloadManager()->specifyDownloadObjective(url,
0559                 pg, WebEnginePartDownloadManager::DownloadObjective::SaveOnly);
0560         }
0561     }
0562     view()->triggerPageAction(QWebEnginePage::DownloadLinkToDisk);
0563 }
0564 
0565 void WebEngineNavigationExtension::slotViewDocumentSource()
0566 {
0567     if (!view())
0568         return;
0569 
0570     const QUrl pageUrl (view()->url());
0571     if (pageUrl.isLocalFile()) {
0572         KIO::OpenUrlJob *job = new KIO::OpenUrlJob(pageUrl, QL1S("text/plain"));
0573         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, view()));
0574         job->start();
0575     } else {
0576         view()->page()->toHtml([this](const QString& html) {
0577             QTemporaryFile tempFile;
0578             tempFile.setFileTemplate(tempFile.fileTemplate() + QL1S(".html"));
0579             tempFile.setAutoRemove(false);
0580             if (tempFile.open()) {
0581                 tempFile.write(html.toUtf8());
0582                 tempFile.close();
0583                 KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile.fileName()), QL1S("text/plain"));
0584                 job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, view()));
0585                 job->setDeleteTemporaryFile(true);
0586                 job->start();
0587             }
0588         });
0589     }
0590 }
0591 
0592 static bool isMultimediaElement(QWebEngineContextMenuRequest::MediaType mediaType)
0593 {
0594     switch(mediaType)
0595     {
0596         case QWebEngineContextMenuRequest::MediaTypeVideo:
0597         case QWebEngineContextMenuRequest::MediaTypeAudio:
0598             return true;
0599         default:
0600             return false;
0601     }
0602 }
0603 
0604 void WebEngineNavigationExtension::slotLoopMedia()
0605 {
0606     if (!view()) {
0607         return;
0608     }
0609 
0610     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0611     if (!isMultimediaElement( data->mediaType()))
0612         return;
0613     view()->page()->triggerAction(QWebEnginePage::ToggleMediaLoop);
0614 }
0615 
0616 void WebEngineNavigationExtension::slotMuteMedia()
0617 {
0618     if (!view()) {
0619         return;
0620     }
0621 
0622     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0623     if (!isMultimediaElement( data->mediaType()))
0624         return;
0625     view()->page()->triggerAction(QWebEnginePage::ToggleMediaMute);
0626 }
0627 
0628 void WebEngineNavigationExtension::slotPlayMedia()
0629 {
0630     if (!view()) {
0631         return;
0632     }
0633 
0634     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0635     if (!isMultimediaElement( data->mediaType()))
0636         return;
0637     view()->page()->triggerAction(QWebEnginePage::ToggleMediaPlayPause);
0638 }
0639 
0640 void WebEngineNavigationExtension::slotShowMediaControls()
0641 {
0642     if (!view()) {
0643         return;
0644     }
0645 
0646     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0647     if (!isMultimediaElement( data->mediaType()))
0648         return;
0649     view()->page()->triggerAction(QWebEnginePage::ToggleMediaControls);
0650 }
0651 
0652 #if 0
0653 static QUrl mediaUrlFrom(QWebElement& element)
0654 {
0655     QWebFrame* frame = element.webFrame();
0656     QString src = frame ? element.attribute(QL1S("src")) : QString();
0657     if (src.isEmpty())
0658         src = frame ? element.evaluateJavaScript(QL1S("this.src")).toString() : QString();
0659 
0660     if (src.isEmpty())
0661         return QUrl();
0662 
0663     return QUrl(frame->baseUrl().resolved(QUrl::fromEncoded(QUrl::toPercentEncoding(src), QUrl::StrictMode)));
0664 }
0665 #endif
0666 
0667 void WebEngineNavigationExtension::slotSaveMedia()
0668 {
0669     WebEnginePage *pg = page();
0670     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0671     if (!isMultimediaElement( data->mediaType())) {
0672         return;
0673     }
0674     if (pg) {
0675         if (data->mediaUrl().isValid()) {
0676             WebEnginePartControls::self()->downloadManager()->specifyDownloadObjective(data->mediaUrl(), pg,
0677                 WebEnginePartDownloadManager::DownloadObjective::SaveOnly);
0678         }
0679         pg->triggerAction(QWebEnginePage::DownloadMediaToDisk);
0680     }
0681 }
0682 
0683 void WebEngineNavigationExtension::slotCopyMedia()
0684 {
0685     if (!view()) {
0686         return;
0687     }
0688     const QWebEngineContextMenuRequest *data =  view()->contextMenuResult();
0689     if (!isMultimediaElement( data->mediaType()))
0690         return;
0691 
0692     QUrl safeURL(data->mediaUrl());
0693     if (!safeURL.isValid())
0694         return;
0695 
0696     safeURL.setPassword(QString());
0697     // Set it in both the mouse selection and in the clipboard
0698     QMimeData* mimeData = new QMimeData;
0699 //TODO: Porting: test
0700     QList<QUrl> safeURLList;
0701     safeURLList.append(safeURL);
0702     mimeData->setUrls(safeURLList);
0703     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
0704 
0705     mimeData = new QMimeData;
0706     mimeData->setUrls(safeURLList);
0707     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Selection);
0708 }
0709 
0710 void WebEngineNavigationExtension::slotTextDirectionChanged()
0711 {
0712     QAction* action = qobject_cast<QAction*>(sender());
0713     if (action) {
0714         bool ok = false;
0715         const int value = action->data().toInt(&ok);
0716         if (ok) {
0717             view()->triggerPageAction(static_cast<QWebEnginePage::WebAction>(value));
0718         }
0719     }
0720 }
0721 
0722 void WebEngineNavigationExtension::slotCheckSpelling()
0723 {
0724     view()->page()->runJavaScript(QL1S("this.value"), [this](const QVariant &value) {
0725         if (!value.isValid()) {
0726             return;
0727         }
0728         const QString text = value.toString();
0729         if (!text.isEmpty()) {
0730             m_spellTextSelectionStart = 0;
0731             m_spellTextSelectionEnd = 0;
0732 
0733             Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker;
0734             Sonnet::Dialog* spellDialog = new Sonnet::Dialog(backgroundSpellCheck, view());
0735             backgroundSpellCheck->setParent(spellDialog);
0736             spellDialog->setAttribute(Qt::WA_DeleteOnClose, true);
0737             spellDialog->showSpellCheckCompletionMessage(true);
0738             connect(spellDialog, &Sonnet::Dialog::replace, this, &WebEngineNavigationExtension::spellCheckerCorrected);
0739             connect(spellDialog, &Sonnet::Dialog::misspelling, this, &WebEngineNavigationExtension::spellCheckerMisspelling);
0740             spellDialog->setBuffer(text);
0741             spellDialog->show();
0742         }
0743     });
0744 }
0745 
0746 void WebEngineNavigationExtension::slotSpellCheckSelection()
0747 {
0748     view()->page()->runJavaScript(QL1S("this.value"), [this](const QVariant &value) {
0749         if (!value.isValid()) {
0750             return;
0751         }
0752         const QString text = value.toString();
0753         if (!text.isEmpty()) {
0754             view()->page()->runJavaScript(QL1S("this.selectionStart + ' ' + this.selectionEnd"), [this, text](const QVariant &value) {
0755                 if (!value.isValid()) {
0756                     return;
0757                 }
0758                 const QString values = value.toString();
0759                 const int pos = values.indexOf(' ');
0760                 m_spellTextSelectionStart = qMax(0, QStringView{values}.left(pos).toInt());
0761                 m_spellTextSelectionEnd = qMax(0, QStringView{values}.mid(pos + 1).toInt());
0762                 // qCDebug(WEBENGINEPART_LOG) << "selection start:" << m_spellTextSelectionStart << "end:" << m_spellTextSelectionEnd;
0763 
0764                 Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker;
0765                 Sonnet::Dialog* spellDialog = new Sonnet::Dialog(backgroundSpellCheck, view());
0766                 backgroundSpellCheck->setParent(spellDialog);
0767                 spellDialog->setAttribute(Qt::WA_DeleteOnClose, true);
0768                 spellDialog->showSpellCheckCompletionMessage(true);
0769                 connect(spellDialog, &Sonnet::Dialog::replace, this, &WebEngineNavigationExtension::spellCheckerCorrected);
0770                 connect(spellDialog, &Sonnet::Dialog::misspelling, this, &WebEngineNavigationExtension::spellCheckerMisspelling);
0771                 connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &WebEngineNavigationExtension::slotSpellCheckDone);
0772                 spellDialog->setBuffer(text.mid(m_spellTextSelectionStart, (m_spellTextSelectionEnd - m_spellTextSelectionStart)));
0773                 spellDialog->show();
0774             });
0775         }
0776     });
0777 }
0778 
0779 void WebEngineNavigationExtension::spellCheckerCorrected(const QString& original, int pos, const QString& replacement)
0780 {
0781     // Adjust the selection end...
0782     if (m_spellTextSelectionEnd > 0) {
0783         m_spellTextSelectionEnd += qMax (0, (replacement.length() - original.length()));
0784     }
0785 
0786     const int index = pos + m_spellTextSelectionStart;
0787     QString script(QL1S("this.value=this.value.substring(0,"));
0788     script += QString::number(index);
0789     script += QL1S(") + \"");
0790     script +=  replacement;
0791     script += QL1S("\" + this.value.substring(");
0792     script += QString::number(index + original.length());
0793     script += QL1S(")");
0794 
0795     //qCDebug(WEBENGINEPART_LOG) << "**** script:" << script;
0796     view()->page()->runJavaScript(script);
0797 }
0798 
0799 void WebEngineNavigationExtension::spellCheckerMisspelling(const QString& text, int pos)
0800 {
0801     // qCDebug(WEBENGINEPART_LOG) << text << pos;
0802     QString selectionScript (QL1S("this.setSelectionRange("));
0803     selectionScript += QString::number(pos + m_spellTextSelectionStart);
0804     selectionScript += QL1C(',');
0805     selectionScript += QString::number(pos + text.length() + m_spellTextSelectionStart);
0806     selectionScript += QL1C(')');
0807     view()->page()->runJavaScript(selectionScript);
0808 }
0809 
0810 void WebEngineNavigationExtension::slotSpellCheckDone(const QString&)
0811 {
0812     // Restore the text selection if one was present before we started the
0813     // spell check.
0814     if (m_spellTextSelectionStart > 0 || m_spellTextSelectionEnd > 0) {
0815         QString script (QL1S("; this.setSelectionRange("));
0816         script += QString::number(m_spellTextSelectionStart);
0817         script += QL1C(',');
0818         script += QString::number(m_spellTextSelectionEnd);
0819         script += QL1C(')');
0820         view()->page()->runJavaScript(script);
0821     }
0822 }
0823 
0824 void WebEngineNavigationExtension::saveHistory()
0825 {
0826     QWebEngineHistory* history = (view() ? view()->history() : nullptr);
0827 
0828     if (history && history->count() > 0) {
0829         //qCDebug(WEBENGINEPART_LOG) << "Current history: index=" << history->currentItemIndex() << "url=" << history->currentItem().url();
0830         QByteArray histData;
0831         QBuffer buff (&histData);
0832         m_historyData.clear();
0833         if (buff.open(QIODevice::WriteOnly)) {
0834             QDataStream stream (&buff);
0835             stream << *history;
0836             m_historyData = qCompress(histData, 9);
0837         }
0838         QWidget* mainWidget = m_part ? m_part->widget() : nullptr;
0839         QWidget* frameWidget = mainWidget ? mainWidget->parentWidget() : nullptr;
0840         if (frameWidget) {
0841             emit saveHistory(frameWidget, m_historyData);
0842             // qCDebug(WEBENGINEPART_LOG) << "# of items:" << history->count() << "current item:" << history->currentItemIndex() << "url:" << history->currentItem().url();
0843         }
0844     } else {
0845         Q_ASSERT(false); // should never happen!!!
0846     }
0847 }
0848 
0849 void WebEngineNavigationExtension::slotPrintPreview()
0850 {
0851     QPrinter printer;
0852     QPrintPreviewDialog dlg(&printer, view());
0853     auto printPreview = [this](QPrinter *p){
0854         QEventLoop loop;
0855         auto preview = [&](bool) {loop.quit();};
0856 #if QT_VERSION_MAJOR < 6
0857         m_view->page()->print(p, preview);
0858 #else
0859         m_view->print(p);
0860         connect(m_view, &QWebEngineView::printFinished, &loop, preview);
0861 #endif
0862         loop.exec();
0863     };
0864     connect(&dlg, &QPrintPreviewDialog::paintRequested, this, printPreview);
0865     dlg.exec();
0866 }
0867 
0868 void WebEngineNavigationExtension::slotOpenSelection()
0869 {
0870     QAction *action = qobject_cast<QAction*>(sender());
0871     if (action) {
0872         BrowserArguments browserArgs;
0873         browserArgs.frameName = QStringLiteral("_blank");
0874 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0875         emit openUrlRequest(QUrl(action->data().toUrl()), KParts::OpenUrlArguments(), browserArgs);
0876 #else
0877         emit browserOpenUrlRequest(QUrl(action->data().toUrl()), KParts::OpenUrlArguments(), browserArgs);
0878 #endif
0879     }
0880 }
0881 
0882 void WebEngineNavigationExtension::slotLinkInTop()
0883 {
0884     if (!view()) {
0885         return;
0886     }
0887 
0888     KParts::OpenUrlArguments uargs;
0889     uargs.setActionRequestedByUser(true);
0890 
0891     BrowserArguments bargs;
0892     bargs.frameName = QL1S("_top");
0893 
0894     const QUrl url(view()->contextMenuResult()->linkUrl());
0895 
0896 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0897     emit openUrlRequest(url, uargs, bargs);
0898 #else
0899     emit browserOpenUrlRequest(url, uargs, bargs);
0900 #endif
0901 }
0902 
0903 ////
0904 
0905 WebEngineTextExtension::WebEngineTextExtension(WebEnginePart* part)
0906     : TextExtension(part)
0907 {
0908     connect(part->view(), &QWebEngineView::selectionChanged, this, &TextExtension::selectionChanged);
0909 }
0910 
0911 WebEnginePart* WebEngineTextExtension::part() const
0912 {
0913     return static_cast<WebEnginePart*>(parent());
0914 }
0915 
0916 bool WebEngineTextExtension::hasSelection() const
0917 {
0918     return part()->view()->hasSelection();
0919 }
0920 
0921 QString WebEngineTextExtension::selectedText(Format format) const
0922 {
0923     switch(format) {
0924     case PlainText:
0925         return part()->view()->selectedText();
0926     case HTML:
0927         // PORTING_TODO selectedText might not be html
0928         return part()->view()->selectedText();
0929     }
0930     return QString();
0931 }
0932 
0933 QString WebEngineTextExtension::completeText(Format format) const
0934 {
0935     // TODO David will hunt me down with a rusty spork if he sees this
0936     QEventLoop ev;
0937     QString str;
0938     switch(format) {
0939     case PlainText:
0940         part()->view()->page()->toPlainText([&ev,&str](const QString& data) {
0941             str = data;
0942             ev.quit();
0943         });
0944         break;
0945     case HTML:
0946         part()->view()->page()->toHtml([&ev,&str](const QString& data) {
0947             str = data;
0948             ev.quit();
0949         });
0950         break;
0951     }
0952     ev.exec();
0953     return str;
0954 }
0955 
0956 ////
0957 
0958 WebEngineHtmlExtension::WebEngineHtmlExtension(WebEnginePart* part)
0959     : HtmlExtension(part)
0960 {
0961 }
0962 
0963 QUrl WebEngineHtmlExtension::baseUrl() const
0964 {
0965     return part()->view()->page()->url();
0966 }
0967 
0968 bool WebEngineHtmlExtension::hasSelection() const
0969 {
0970     return part()->view()->hasSelection();
0971 }
0972 
0973 AsyncSelectorInterface::QueryMethods WebEngineHtmlExtension::supportedAsyncQueryMethods() const
0974 {
0975     return AsyncSelectorInterface::EntireContent;
0976 }
0977 
0978 QList<AsyncSelectorInterface::Element> WebEngineHtmlExtension::jsonToElementList(const QVariant& json)
0979 {
0980     QList<AsyncSelectorInterface::Element> res;
0981     QJsonDocument doc = QJsonDocument::fromVariant(json);
0982     if (!doc.isArray()) {
0983         return res;
0984     }
0985 
0986     QJsonArray array = doc.array();
0987     std::transform(array.constBegin(), array.constEnd(), std::back_inserter(res), [](const QJsonValue &val){return WebEngineHtmlExtension::jsonToElement(val.toObject());});
0988     return res;
0989 }
0990 
0991 AsyncSelectorInterface::Element WebEngineHtmlExtension::jsonToElement(const QVariant& json)
0992 {
0993     QJsonDocument doc = QJsonDocument::fromVariant(json);
0994     if (!doc.isObject()) {
0995         return AsyncSelectorInterface::Element();
0996     }
0997     QJsonObject obj = doc.object();
0998     return jsonToElement(obj);
0999 }
1000 
1001 AsyncSelectorInterface::Element WebEngineHtmlExtension::jsonToElement(const QJsonObject& obj)
1002 {
1003     AsyncSelectorInterface::Element res;
1004     QJsonValue nameVal = obj.value(QLatin1String("tag"));
1005     if (nameVal.isUndefined()) {
1006         return res;
1007     }
1008     res.setTagName(nameVal.toString());
1009     QVariantHash attributes = obj.value(QLatin1String("attributes")).toObject().toVariantHash();
1010     for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
1011         res.setAttribute(it.key(), it.value().toString());
1012     }
1013     return res;
1014 }
1015 
1016 void WebEngineHtmlExtension::querySelectorAllAsync(const QString& query, AsyncSelectorInterface::QueryMethod method, MultipleElementSelectorCallback& callback)
1017 {
1018     QList<Element> result;
1019     if (method == None || !part() || !part()->page() || !(supportedAsyncQueryMethods() & method)) {
1020         callback(result);
1021         return;
1022     }
1023 
1024     auto internalCallback = [callback] (const QVariant &res) {
1025         if (!res.isValid()) {
1026             return;
1027         }
1028         callback(WebEngineHtmlExtension::jsonToElementList(res));
1029     };
1030 
1031     static const QString s_allSelectorTemplate = QStringLiteral("querySelectorAllToList(\"%1\")");
1032     QString fullQuery = s_allSelectorTemplate.arg(query);
1033     // QString test = "function findElements(sel) {\nvar list = document.querySelectorAll(sel);var result = []; for (const e of list) { var obj = {'tag': e.tagName, 'attributes' : {}}; for (const a of e.attributes) { obj.attributes[a.name] = a.value; } result.push(obj); } return result; } findElements(%1);";
1034     part()->page()->runJavaScript(fullQuery, QWebEngineScript::ApplicationWorld, internalCallback);
1035 }
1036 
1037 void WebEngineHtmlExtension::querySelectorAsync(const QString& query, AsyncSelectorInterface::QueryMethod method, SingleElementSelectorCallback& callback)
1038 {
1039     Element result;
1040     if (method == AsyncSelectorInterface::None || !part() || !part()->page() || !(supportedAsyncQueryMethods() & method)) {
1041         callback(result);
1042         return;
1043     }
1044 
1045     auto internalCallback = [callback] (const QVariant &res) {
1046         if (!res.isValid()) {
1047             return;
1048         }
1049         callback(WebEngineHtmlExtension::jsonToElement(res));
1050     };
1051 
1052     static const QString s_selectorTemplate = QStringLiteral("querySelectorToObject(\"%1\")");
1053     QString fullQuery = s_selectorTemplate.arg(query);
1054     part()->page()->runJavaScript(fullQuery, QWebEngineScript::ApplicationWorld, internalCallback);
1055 }
1056 
1057 QVariant WebEngineHtmlExtension::htmlSettingsProperty(HtmlSettingsInterface::HtmlSettingsType type) const
1058 {
1059     QWebEngineView* view = part() ? part()->view() : nullptr;
1060     QWebEnginePage* page = view ? view->page() : nullptr;
1061     QWebEngineSettings* settings = page ? page->settings() : nullptr;
1062 
1063     if (settings) {
1064         switch (type) {
1065         case HtmlSettingsInterface::AutoLoadImages:
1066             return settings->testAttribute(QWebEngineSettings::AutoLoadImages);
1067         case HtmlSettingsInterface::JavaEnabled:
1068             return false; // settings->testAttribute(QWebEngineSettings::JavaEnabled);
1069         case HtmlSettingsInterface::JavascriptEnabled:
1070             return settings->testAttribute(QWebEngineSettings::JavascriptEnabled);
1071         case HtmlSettingsInterface::PluginsEnabled:
1072             return settings->testAttribute(QWebEngineSettings::PluginsEnabled);
1073         case HtmlSettingsInterface::DnsPrefetchEnabled:
1074             return false; //settings->testAttribute(QWebEngineSettings::DnsPrefetchEnabled);
1075         case HtmlSettingsInterface::MetaRefreshEnabled:
1076             return view->pageAction(QWebEnginePage::Stop)->isEnabled();
1077         case HtmlSettingsInterface::LocalStorageEnabled:
1078             return settings->testAttribute(QWebEngineSettings::LocalStorageEnabled);
1079         case HtmlSettingsInterface::OfflineStorageDatabaseEnabled:
1080             return false; //settings->testAttribute(QWebEngineSettings::OfflineStorageDatabaseEnabled);
1081         case HtmlSettingsInterface::OfflineWebApplicationCacheEnabled:
1082             return false ;//settings->testAttribute(QWebEngineSettings::OfflineWebApplicationCacheEnabled);
1083         case HtmlSettingsInterface::PrivateBrowsingEnabled:
1084             return false; //settings->testAttribute(QWebEngineSettings::PrivateBrowsingEnabled);
1085         case HtmlSettingsInterface::UserDefinedStyleSheetURL:
1086             return false; //settings->userStyleSheetUrl();
1087         default:
1088             break;
1089         }
1090     }
1091 
1092     return QVariant();
1093 }
1094 
1095 bool WebEngineHtmlExtension::setHtmlSettingsProperty(HtmlSettingsInterface::HtmlSettingsType type, const QVariant& value)
1096 {
1097     QWebEngineView* view = part() ? part()->view() : nullptr;
1098     QWebEnginePage* page = view ? view->page() : nullptr;
1099     QWebEngineSettings* settings = page ? page->settings() : nullptr;
1100 
1101     if (settings) {
1102         switch (type) {
1103         case HtmlSettingsInterface::AutoLoadImages:
1104             settings->setAttribute(QWebEngineSettings::AutoLoadImages, value.toBool());
1105             return true;
1106         case HtmlSettingsInterface::JavaEnabled:
1107             //settings->setAttribute(QWebESettings::JavaEnabled, value.toBool());
1108             return false;
1109         case HtmlSettingsInterface::JavascriptEnabled:
1110             settings->setAttribute(QWebEngineSettings::JavascriptEnabled, value.toBool());
1111             return true;
1112         case HtmlSettingsInterface::PluginsEnabled:
1113             settings->setAttribute(QWebEngineSettings::PluginsEnabled, value.toBool());
1114             return true;
1115         case HtmlSettingsInterface::DnsPrefetchEnabled:
1116 //            settings->setAttribute(QWebEngineSettings::DnsPrefetchEnabled, value.toBool());
1117             return false;
1118         case HtmlSettingsInterface::MetaRefreshEnabled:
1119             view->triggerPageAction(QWebEnginePage::Stop);
1120             return true;
1121         case HtmlSettingsInterface::LocalStorageEnabled:
1122             settings->setAttribute(QWebEngineSettings::LocalStorageEnabled, value.toBool());
1123             return false;
1124         case HtmlSettingsInterface::OfflineStorageDatabaseEnabled:
1125             //settings->setAttribute(QWebEngineSettings::OfflineStorageDatabaseEnabled, value.toBool());
1126             return false;
1127         case HtmlSettingsInterface::OfflineWebApplicationCacheEnabled:
1128             //settings->setAttribute(QWebEngineSettings::OfflineWebApplicationCacheEnabled, value.toBool());
1129             return false;
1130         case HtmlSettingsInterface::PrivateBrowsingEnabled:
1131             //settings->setAttribute(QWebEnngineSettings::PrivateBrowsingEnabled, value.toBool());
1132             return false;
1133         case HtmlSettingsInterface::UserDefinedStyleSheetURL:
1134             //qCDebug(WEBENGINEPART_LOG) << "Setting user style sheet for" << page << "to" << value.toUrl();
1135           //  settings->setUserStyleSheetUrl(value.toUrl());
1136             return false;
1137         default:
1138             break;
1139         }
1140     }
1141 
1142     return false;
1143 }
1144 
1145 WebEnginePart* WebEngineHtmlExtension::part() const
1146 {
1147     return static_cast<WebEnginePart*>(parent());
1148 }
1149 
1150 WebEngineDownloaderExtension::WebEngineDownloaderExtension(WebEnginePart* parent) : DownloaderExtension(parent)
1151 {
1152 }
1153 
1154 WebEngineDownloaderExtension::~WebEngineDownloaderExtension()
1155 {
1156 }
1157 
1158 KParts::ReadOnlyPart* WebEngineDownloaderExtension::part() const
1159 {
1160     return qobject_cast<WebEnginePart*>(parent());
1161 }
1162 
1163 void WebEngineDownloaderExtension::addDownloadRequest(QWebEngineDownloadRequest* req)
1164 {
1165     QUrl url = req->url();
1166     m_downloadRequests.insert(url, req);
1167     auto removeRequest = [this, url] (QObject *obj) {m_downloadRequests.remove(url, dynamic_cast<QWebEngineDownloadRequest*>(obj));};
1168     connect(req, &QWebEngineDownloadRequest::destroyed, this, removeRequest);
1169 }
1170 
1171 KonqInterfaces::DownloaderJob* WebEngineDownloaderExtension::downloadJob(const QUrl& url, quint32 id, QObject* parent)
1172 {
1173     auto items = m_downloadRequests.values(url);
1174     if (items.isEmpty()) {
1175         return nullptr;
1176     }
1177     auto it = std::find_if(items.constBegin(), items.constEnd(), [id](QWebEngineDownloadRequest* it){return it->id() == id;});
1178     //If no job with the given ID is found, return the last one, which is the first created
1179     QWebEngineDownloadRequest *item = it != items.constEnd() ? *it : items.last();
1180     return new WebEngineDownloadJob(item, parent);
1181 }