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

0001 /*
0002     This file is part of the KDE project.
0003 
0004     SPDX-FileCopyrightText: 2017 Stefano Crocco <stefano.crocco@alice.it>
0005 
0006     SPDX-License-Identifier: LGPL-2.1-or-later
0007 */
0008 
0009 #include "webenginepartdownloadmanager.h"
0010 
0011 #include "webenginepage.h"
0012 #include <webenginepart_debug.h>
0013 #include "webenginepartcontrols.h"
0014 #include "navigationrecorder.h"
0015 #include "choosepagesaveformatdlg.h"
0016 
0017 #include "libkonq_utils.h"
0018 
0019 #include <QWebEngineView>
0020 #include <QWebEngineProfile>
0021 #include <QFileDialog>
0022 #include <QStandardPaths>
0023 #include <QFileInfo>
0024 #include <QMimeDatabase>
0025 #include <QMimeType>
0026 #include <QTimer>
0027 #include <QDialog>
0028 
0029 #include <KLocalizedString>
0030 #include <KJobTrackerInterface>
0031 #include <KIO/JobTracker>
0032 #include <KIO/OpenUrlJob>
0033 #include <KFileUtils>
0034 #include <KIO/JobUiDelegate>
0035 #include <KIO/JobUiDelegateFactory>
0036 
0037 using namespace KonqInterfaces;
0038 
0039 QTemporaryDir& WebEnginePartDownloadManager::tempDownloadDir()
0040 {
0041     static QTemporaryDir s_tempDownloadDir(QDir(QDir::tempPath()).filePath(QStringLiteral("WebEnginePartDownloadManager")));
0042     return s_tempDownloadDir;
0043 }
0044 
0045 WebEnginePartDownloadManager::WebEnginePartDownloadManager(QWebEngineProfile *profile, QObject *parent)
0046     : QObject(parent)
0047 {
0048     connect(profile, &QWebEngineProfile::downloadRequested, this, &WebEnginePartDownloadManager::performDownload);
0049 }
0050 
0051 WebEnginePartDownloadManager::~WebEnginePartDownloadManager()
0052 {
0053 }
0054 
0055 void WebEnginePartDownloadManager::specifyDownloadObjective(const QUrl& url, WebEnginePage *page, DownloadObjective objective)
0056 {
0057     m_downloadObjectives.insert(url, {page, objective});
0058 }
0059 
0060 WebEnginePartDownloadManager::DownloadObjective WebEnginePartDownloadManager::fetchDownloadObjective(const QUrl &url, WebEnginePage* page)
0061 {
0062     DownloadObjective objective = DownloadObjective::OpenInApplication;
0063     auto it = m_downloadObjectives.constFind(url);
0064     if (it == m_downloadObjectives.constEnd()) {
0065         return objective;
0066     }
0067 
0068     for (; it != m_downloadObjectives.constEnd() || it.key() != url; ++it) {
0069         if (it.value().page == page) {
0070             objective = it.value().downloadObjective;
0071             break;
0072         }
0073     }
0074     if (it != m_downloadObjectives.constEnd()) {
0075         m_downloadObjectives.remove(url, it.value());
0076     }
0077     return objective;
0078 }
0079 
0080 static QStringList supportedMimetypes() {
0081 
0082     //Check whether the mimetypes is one known to be displayed by QtWebEngine itself. If so, it means
0083     //that the file should be saved (otherwise, QtWebEngine would have displayed it and not requested
0084     //a download.
0085     //TODO: find out all the mimetypes supported by QtWebEngine
0086     static const QStringList s_supportedMimetypes = {
0087         QStringLiteral("audio/mp4"),
0088         QStringLiteral("audio/mpeg"),
0089         QStringLiteral("audio/ogg"),
0090         QStringLiteral("image/bmp"),
0091         QStringLiteral("image/gif"),
0092         QStringLiteral("image/jpeg"),
0093         QStringLiteral("image/png"),
0094         QStringLiteral("image/svg+xml"),
0095         QStringLiteral("video/mp4"),
0096         QStringLiteral("video/ogg"),
0097         QStringLiteral("application/xml"),
0098         QStringLiteral("text/plain"),
0099         QStringLiteral("text/html"),
0100         QStringLiteral("text/xml"),
0101         QStringLiteral("text/markdown"),
0102     };
0103     return s_supportedMimetypes;
0104 }
0105 
0106 void WebEnginePartDownloadManager::addPage(WebEnginePage* page)
0107 {
0108     if (!m_pages.contains(page)) {
0109         m_pages.append(page);
0110     }
0111     connect(page, &QObject::destroyed, this, &WebEnginePartDownloadManager::removePage);
0112 }
0113 
0114 void WebEnginePartDownloadManager::removePage(QObject* page)
0115 {
0116     m_pages.removeOne(static_cast<WebEnginePage*>(page));
0117 }
0118 
0119 void WebEnginePartDownloadManager::performDownload(QWebEngineDownloadRequest* it)
0120 {
0121     QUrl url = it->url();
0122     WebEnginePage *page = qobject_cast<WebEnginePage*>(it->page());
0123     if (it->isSavePageDownload()) {
0124         saveHtmlPage(it, page);
0125         return;
0126     }
0127     bool forceNew = false;
0128     //According to the documentation, QWebEngineDownloadRequest::page() can return nullptr "if the download was not triggered by content in a page"
0129     if (!page && !m_pages.isEmpty()) {
0130         qCDebug(WEBENGINEPART_LOG) << "downloading" << url << "in new window or tab";
0131         page = m_pages.first();
0132         forceNew = true;
0133     } else if (!page) {
0134         qCDebug(WEBENGINEPART_LOG) << "Couldn't find a part wanting to download" << url;
0135         it->cancel();
0136         return;
0137     }
0138 
0139     if (WebEnginePartControls::self()->navigationRecorder()->isPostRequest(it->url(), page)) {
0140         WebEnginePartControls::self()->navigationRecorder()->recordNavigationFinished(page, url);
0141         //When downloading the reply to a POST request, it's better to use a new tab, as navigating back to POST requests
0142         //can be problematic
0143         forceNew = true;
0144     }
0145 
0146     DownloadObjective objective = fetchDownloadObjective(it->url(), page);
0147     //If the mimetype is supported and the objective is OpenInApplication, it means that, for whatever reason,
0148     //QtWebEngine decided it doesn't want to display the URL: most likely, this means that it should be saved
0149     //(for example, because of "attachment" Content-Disposition header)
0150     if (objective == DownloadObjective::OpenInApplication && supportedMimetypes().contains(it->mimeType())) {
0151         objective = DownloadObjective::SaveOnly;
0152     }
0153 
0154     it->setDownloadDirectory(tempDownloadDir().path());
0155     QMimeDatabase db;
0156     QMimeType type = db.mimeTypeForName(it->mimeType());
0157     QString suggestedName = it->suggestedFileName();
0158     QString fileName = generateDownloadTempFileName(suggestedName, type.preferredSuffix());
0159     it->setDownloadFileName(fileName);
0160 
0161     page->requestDownload(it, forceNew, objective);
0162 }
0163 
0164 QString WebEnginePartDownloadManager::generateDownloadTempFileName(const QString& suggestedName, const QString& ext)
0165 {
0166     QString baseName(suggestedName);
0167     if (baseName.isEmpty()) {
0168         baseName = QString::number(QTime::currentTime().msecsSinceStartOfDay());
0169     }
0170     if (QFileInfo(baseName).completeSuffix().isEmpty() && !ext.isEmpty()) {
0171         baseName.append("."+ext);
0172     }
0173     QString completeName = QDir(tempDownloadDir().path()).filePath(baseName);
0174     if (QFileInfo::exists(completeName)) {
0175         completeName = KFileUtils::suggestName(QUrl::fromLocalFile(tempDownloadDir().path()), baseName);
0176     }
0177     return completeName;
0178 }
0179 
0180 void WebEnginePartDownloadManager::saveHtmlPage(QWebEngineDownloadRequest* it, WebEnginePage *page)
0181 {
0182     QWidget *parent = page ? page->view() : nullptr;
0183 
0184     ChoosePageSaveFormatDlg *formatDlg = new ChoosePageSaveFormatDlg(parent);
0185     if (formatDlg->exec() == QDialog::Rejected) {
0186         return;
0187     }
0188     QWebEngineDownloadRequest::SavePageFormat format = formatDlg->choosenFormat();
0189     it->setSavePageFormat(format);
0190 
0191     QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
0192     QFileDialog dlg(parent, QString(), downloadDir);
0193     dlg.setAcceptMode(QFileDialog::AcceptSave);
0194 
0195     QString mimeType = format == QWebEngineDownloadRequest::MimeHtmlSaveFormat ? QStringLiteral("application/x-mimearchive") : QStringLiteral("text/html");
0196 
0197     dlg.setMimeTypeFilters(QStringList{mimeType, "application/octet-stream"});
0198     dlg.selectFile(it->suggestedFileName());
0199 
0200     QDialog::DialogCode exitCode = static_cast<QDialog::DialogCode>(dlg.exec());
0201     if (exitCode == QDialog::Rejected) {
0202         it->cancel();
0203         return;
0204     }
0205 
0206     QString file = dlg.selectedFiles().at(0);
0207     QFileInfo info(file);
0208     QString relativePath = info.fileName();
0209     QString dir = info.dir().path();
0210 
0211     it->setDownloadDirectory(dir);
0212     it->setDownloadFileName(relativePath);
0213     WebEngineDownloadJob *job = new WebEngineDownloadJob(it, this);
0214     KJobTrackerInterface *t = KIO::getJobTracker();
0215     if (t) {
0216         t->registerJob(job);
0217     }
0218     job->start();
0219 }
0220 
0221 WebEngineDownloadJob::WebEngineDownloadJob(QWebEngineDownloadRequest* it, QObject* parent) : DownloaderJob(parent), m_downloadItem(it)
0222 {
0223     setCapabilities(KJob::Killable|KJob::Suspendable);
0224     connect(this, &KJob::result, this, &WebEngineDownloadJob::emitDownloadResult);
0225     connect(m_downloadItem, &QWebEngineDownloadRequest::stateChanged, this, &WebEngineDownloadJob::stateChanged);
0226     setTotalAmount(KJob::Bytes, m_downloadItem->totalBytes());
0227     setFinishedNotificationHidden(true);
0228     setAutoDelete(false);
0229 }
0230 
0231 WebEngineDownloadJob::~WebEngineDownloadJob() noexcept
0232 {
0233     if (m_downloadItem) {
0234         m_downloadItem->deleteLater();
0235         m_downloadItem = nullptr;
0236     }
0237 }
0238 
0239 QUrl WebEngineDownloadJob::url() const
0240 {
0241     return m_downloadItem ? m_downloadItem->url() : QUrl{};
0242 }
0243 
0244 void WebEngineDownloadJob::start()
0245 {
0246     if (m_downloadItem && m_downloadItem->state() == QWebEngineDownloadRequest::DownloadRequested) {
0247         m_downloadItem->accept();
0248     }
0249     QTimer::singleShot(0, this, &WebEngineDownloadJob::startDownloading);
0250 }
0251 
0252 bool WebEngineDownloadJob::finished() const
0253 {
0254     return !m_downloadItem || (m_started && m_downloadItem->isFinished());
0255 }
0256 
0257 bool WebEngineDownloadJob::doKill()
0258 {
0259     m_downloadItem->cancel();
0260     return true;
0261 }
0262 
0263 bool WebEngineDownloadJob::doResume()
0264 {
0265     if (m_downloadItem) {
0266         m_downloadItem->resume();
0267     }
0268     return true;
0269 }
0270 
0271 bool WebEngineDownloadJob::doSuspend()
0272 {
0273     if (m_downloadItem) {
0274         m_downloadItem->pause();
0275     }
0276     return true;
0277 }
0278 
0279 #if QT_VERSION_MAJOR < 6
0280 void WebEngineDownloadJob::downloadProgressed(qint64 received, qint64 total)
0281 {
0282     setPercent(total != 0 ? received*100.0/total : 0);
0283 }
0284 #else
0285 void WebEngineDownloadJob::downloadProgressed()
0286 {
0287     setPercent(m_downloadItem->totalBytes() != 0 ? m_downloadItem->receivedBytes()*100/m_downloadItem->totalBytes() : 0);
0288 }
0289 #endif
0290 
0291 void WebEngineDownloadJob::stateChanged(QWebEngineDownloadRequest::DownloadState state)
0292 {
0293     switch (state) {
0294         case QWebEngineDownloadRequest::DownloadInterrupted:
0295         case QWebEngineDownloadRequest::DownloadCancelled:
0296             setError(m_downloadItem->interruptReason() + UserDefinedError);
0297             setErrorText(m_downloadItem->interruptReasonString());
0298             break;
0299         default: return;
0300     }
0301 }
0302 
0303 QString WebEngineDownloadJob::errorString() const
0304 {
0305     return i18n("An error occurred while saving the file: %1", errorText());
0306 }
0307 
0308 void WebEngineDownloadJob::startDownloading()
0309 {
0310     m_started = true;
0311     if (!m_downloadItem) {
0312         return;
0313     }
0314     m_startTime = QDateTime::currentDateTime();
0315     QString name = m_downloadItem->downloadFileName();
0316     emit description(this, i18nc("Notification about downloading a file", "Downloading"),
0317                     QPair<QString, QString>(i18nc("Source of a file being downloaded", "Source"), m_downloadItem->url().toString()),
0318                     QPair<QString, QString>(i18nc("Destination of a file download", "Destination"), name));
0319     //Between calls to QWebEngineDownloadRequest::accept and QWebEngineDownloadRequest::pause, QtWebEngine already starts downloading the file
0320     //This means that, for small files, it's possible that when WebEngineDownloadJob::start is called, the download will already have been
0321     //completed. In that case, set the download progress to 100% and emit the result() signal
0322     if (!m_downloadItem->isFinished()) {
0323 #if QT_VERSION_MAJOR < 6
0324         connect(m_downloadItem, &QWebEngineDownloadRequest::downloadProgress, this, &WebEngineDownloadJob::downloadProgressed);
0325         connect(m_downloadItem, &QWebEngineDownloadRequest::finished, this, &WebEngineDownloadJob::downloadFinished);
0326 #else
0327         connect(m_downloadItem, &QWebEngineDownloadRequest::receivedBytesChanged, this, &WebEngineDownloadJob::downloadProgressed);
0328         connect(m_downloadItem, &QWebEngineDownloadRequest::isFinishedChanged, this, &WebEngineDownloadJob::downloadFinished);
0329 #endif
0330         m_downloadItem->resume();
0331     } else {
0332 #if QT_VERSION_MAJOR < 6
0333         downloadProgressed(m_downloadItem->receivedBytes(), m_downloadItem->totalBytes());
0334 #else
0335         downloadProgressed();
0336 #endif
0337         emitResult();
0338     }
0339 }
0340 
0341 void WebEngineDownloadJob::downloadFinished()
0342 {
0343     QPointer<WebEnginePage> page = m_downloadItem ? qobject_cast<WebEnginePage*>(m_downloadItem->page()) : nullptr;
0344     emitResult();
0345     QDateTime now = QDateTime::currentDateTime();
0346     if (m_startTime.msecsTo(now) < 500) {
0347         if (page) {
0348             QString filePath = QDir(m_downloadItem->downloadDirectory()).filePath(m_downloadItem->downloadFileName());
0349             emit page->setStatusBarText(i18nc("Finished saving URL", "Saved %1 as %2", m_downloadItem->url().toString(), filePath));
0350         }
0351     }
0352 }
0353 
0354 QString WebEngineDownloadJob::downloadPath() const
0355 {
0356     if (!m_downloadItem) {
0357         return QString();
0358     }
0359     return QDir(m_downloadItem->downloadDirectory()).filePath(m_downloadItem->downloadFileName());
0360 }
0361 
0362 QWebEngineDownloadRequest * WebEngineDownloadJob::item() const
0363 {
0364     return m_downloadItem;
0365 }
0366 
0367 bool WebEngineDownloadJob::setDownloadPath(const QString& path)
0368 {
0369     if (!canChangeDownloadPath()) {
0370         return false;
0371     }
0372     QFileInfo info(path);
0373     m_downloadItem->setDownloadFileName(info.fileName());
0374     m_downloadItem->setDownloadDirectory(info.path());
0375     return true;
0376 }
0377 
0378 bool WebEngineDownloadJob::canChangeDownloadPath() const
0379 {
0380     return m_downloadItem && m_downloadItem->state() == QWebEngineDownloadRequest::DownloadRequested;
0381 }
0382 
0383 void WebEngineDownloadJob::emitDownloadResult(KJob* job)
0384 {
0385     //job is the same as this, except it's not cast to WebEngineDownloadJob
0386     Q_UNUSED(job);
0387 
0388     QUrl resultUrl = error() == 0 ? QUrl::fromLocalFile(downloadPath()) : Konq::makeErrorUrl(error(), errorText(), url());
0389     emit downloadResult(this, resultUrl);
0390 }