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 }