File indexing completed on 2024-04-28 16:51:32
0001 /* 0002 SPDX-FileCopyrightText: 2017 Kai Uwe Broulik <kde@privat.broulik.de> 0003 SPDX-FileCopyrightText: 2017 David Edmundson <davidedmundson@kde.org> 0004 0005 SPDX-License-Identifier: MIT 0006 */ 0007 0008 #include "downloadjob.h" 0009 #include "settings.h" 0010 0011 #include <QDateTime> 0012 #include <QGuiApplication> 0013 #include <QJsonObject> 0014 0015 #include <KActivities/ResourceInstance> 0016 #include <KFileMetaData/UserMetaData> 0017 #include <KLocalizedString> 0018 #include <KUiServerV2JobTracker> 0019 0020 #include <KIO/Global> 0021 0022 DownloadJob::DownloadJob() 0023 : KJob() 0024 { 0025 // Tell KJobTracker to show the job right away so that we get a "finished" 0026 // notification even for tiny downloads 0027 setProperty("immediateProgressReporting", true); 0028 0029 // the thing with "canResume" in chrome downloads is that it just means 0030 // "this download can be resumed right now because it is paused", 0031 // it's not a general thing. I think we can always pause/resume downloads 0032 // unless they're canceled/interrupted at which point we don't have a DownloadJob 0033 // anymore anyway 0034 setCapabilities(Killable | Suspendable); 0035 0036 // TODO When suspending on Firefox the download job goes away for some reason?! 0037 // Until I have the virtue to figure that out just disallow suspending downloads on Firefox :) 0038 if (Settings::self().environment() == Settings::Environment::Firefox) { 0039 setCapabilities(Killable); 0040 } 0041 } 0042 0043 void DownloadJob::start() 0044 { 0045 QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection); 0046 } 0047 0048 void DownloadJob::doStart() 0049 { 0050 } 0051 0052 bool DownloadJob::doKill() 0053 { 0054 Q_EMIT killRequested(); 0055 // TODO what if the user kills us from notification area while the 0056 // "Save As" prompt is still open? 0057 return true; 0058 } 0059 0060 bool DownloadJob::doSuspend() 0061 { 0062 Q_EMIT suspendRequested(); 0063 return true; 0064 } 0065 0066 bool DownloadJob::doResume() 0067 { 0068 Q_EMIT resumeRequested(); 0069 return true; 0070 } 0071 0072 QUrl DownloadJob::originUrl() const 0073 { 0074 QUrl url = (m_finalUrl.isValid() ? m_finalUrl : m_url); 0075 0076 if (m_referrer.isValid() && (url.scheme() == QLatin1String("blob") || url.scheme() == QLatin1String("data"))) { 0077 url = m_referrer; 0078 } 0079 0080 return url; 0081 } 0082 0083 void DownloadJob::update(const QJsonObject &payload) 0084 { 0085 auto end = payload.constEnd(); 0086 0087 bool descriptionDirty = false; 0088 0089 const QUrl oldOriginUrl = originUrl(); 0090 0091 auto it = payload.constFind(QStringLiteral("url")); 0092 if (it != end) { 0093 m_url = QUrl(it->toString()); 0094 } 0095 0096 it = payload.constFind(QStringLiteral("finalUrl")); 0097 if (it != end) { 0098 m_finalUrl = QUrl(it->toString()); 0099 } 0100 0101 it = payload.constFind(QStringLiteral("referrer")); 0102 if (it != end) { 0103 m_referrer = QUrl(it->toString()); 0104 } 0105 0106 descriptionDirty = (originUrl() != oldOriginUrl); 0107 0108 it = payload.constFind(QStringLiteral("filename")); 0109 if (it != end) { 0110 m_fileName = it->toString(); 0111 0112 const QUrl destination = QUrl::fromLocalFile(it->toString()); 0113 0114 setProperty("destUrl", destination.toString(QUrl::RemoveFilename | QUrl::StripTrailingSlash)); 0115 0116 if (m_destination != destination) { 0117 m_destination = destination; 0118 descriptionDirty = true; 0119 } 0120 } 0121 0122 it = payload.constFind(QStringLiteral("mime")); 0123 if (it != end) { 0124 m_mimeType = it->toString(); 0125 } 0126 0127 it = payload.constFind(QStringLiteral("danger")); 0128 if (it != end) { 0129 const QString danger = it->toString(); 0130 if (danger == QLatin1String("accepted")) { 0131 // Clears previous danger message 0132 Q_EMIT infoMessage(this, QString()); 0133 } else if (danger != QLatin1String("safe")) { 0134 Q_EMIT infoMessage(this, i18n("This type of file can harm your computer. If you want to keep it, accept this download from the browser window.")); 0135 } 0136 } 0137 0138 it = payload.constFind(QStringLiteral("incognito")); 0139 if (it != end) { 0140 m_incognito = it->toBool(); 0141 } 0142 0143 it = payload.constFind(QStringLiteral("totalBytes")); 0144 if (it != end) { 0145 const qlonglong totalAmount = it->toDouble(); 0146 if (totalAmount > -1) { 0147 setTotalAmount(Bytes, totalAmount); 0148 } 0149 } 0150 0151 const auto oldBytesReceived = m_bytesReceived; 0152 it = payload.constFind(QStringLiteral("bytesReceived")); 0153 if (it != end) { 0154 m_bytesReceived = it->toDouble(); 0155 setProcessedAmount(Bytes, m_bytesReceived); 0156 } 0157 0158 setTotalAmount(Files, 1); 0159 0160 it = payload.constFind(QStringLiteral("paused")); 0161 if (it != end) { 0162 const bool paused = it->toBool(); 0163 0164 if (paused) { 0165 suspend(); 0166 } else { 0167 resume(); 0168 } 0169 } 0170 0171 bool speedValid = false; 0172 qulonglong speed = 0; 0173 it = payload.constFind(QStringLiteral("estimatedEndTime")); 0174 if (it != end) { 0175 // now calculate the speed from estimated end time and total size 0176 // funny how chrome only gives us a time whereas KJob operates on speed 0177 // and calculates the time this way :) 0178 0179 const QDateTime endTime = QDateTime::fromString(it->toString(), Qt::ISODate); 0180 if (endTime.isValid()) { 0181 const QDateTime now = QDateTime::currentDateTimeUtc(); 0182 0183 qulonglong remainingBytes = totalAmount(Bytes) - processedAmount(Bytes); 0184 quint64 remainingTime = now.secsTo(endTime); 0185 0186 if (remainingTime > 0) { 0187 speed = remainingBytes / remainingTime; 0188 speedValid = true; 0189 m_fallbackSpeedTimer.invalidate(); 0190 } 0191 } 0192 } 0193 0194 if (!speedValid) { 0195 if (!m_fallbackSpeedTimer.isValid()) { 0196 m_fallbackSpeedTimer.start(); 0197 } 0198 0199 // When download size isn't known, we don't get estimatedEndTime but there's 0200 // also no dedicated speed field, so we'll have to calculate that ourself now 0201 if (m_fallbackSpeedTimer.hasExpired(990)) { // not exactly 1000ms to account for some fuzziness 0202 const auto deltaBytes = m_bytesReceived - oldBytesReceived; 0203 speed = (deltaBytes * 1000) / m_fallbackSpeedTimer.elapsed(); 0204 speedValid = true; 0205 m_fallbackSpeedTimer.start(); 0206 } 0207 } 0208 0209 if (speedValid) { 0210 emitSpeed(speed); 0211 } 0212 0213 if (descriptionDirty) { 0214 updateDescription(); 0215 } 0216 0217 const QString error = payload.value(QStringLiteral("error")).toString(); 0218 if (!error.isEmpty()) { 0219 if (error == QLatin1String("USER_CANCELED") || error == QLatin1String("USER_SHUTDOWN")) { 0220 setError(KIO::ERR_USER_CANCELED); // will keep Notification applet from showing a "finished"/error message 0221 emitResult(); 0222 return; 0223 } 0224 0225 // value is a QVariant so we can be lazy and support both KIO errors and custom test 0226 // if QVariant is an int: use that as KIO error 0227 // if QVariant is a QString: set UserError and message 0228 static const QHash<QString, QString> errors{ 0229 // for a list of these error codes *and their meaning* instead of looking at browser 0230 // extension docs, check out Chromium's source code: download_interrupt_reason_values.h 0231 {QStringLiteral("FILE_ACCESS_DENIED"), i18n("Access denied.")}, // KIO::ERR_ACCESS_DENIED 0232 {QStringLiteral("FILE_NO_SPACE"), i18n("Insufficient free space.")}, // KIO::ERR_DISK_FULL 0233 {QStringLiteral("FILE_NAME_TOO_LONG"), i18n("The file name you have chosen is too long.")}, 0234 {QStringLiteral("FILE_TOO_LARGE"), i18n("The file is too large to be downloaded.")}, 0235 // haha 0236 {QStringLiteral("FILE_VIRUS_INFECTED"), i18n("The file possibly contains malicious contents.")}, 0237 {QStringLiteral("FILE_TRANSIENT_ERROR"), i18n("A temporary error has occurred. Please try again later.")}, 0238 0239 {QStringLiteral("NETWORK_FAILED"), i18n("A network error has occurred.")}, 0240 {QStringLiteral("NETWORK_TIMEOUT"), i18n("The network operation timed out.")}, // TODO something less geeky 0241 {QStringLiteral("NETWORK_DISCONNECTED"), i18n("The network connection has been lost.")}, 0242 {QStringLiteral("NETWORK_SERVER_DOWN"), i18n("The server is no longer reachable.")}, 0243 0244 {QStringLiteral("SERVER_FAILED"), i18n("A server error has occurred.")}, 0245 // chromium code says "internal use" and this is really not something the user should see 0246 // SERVER_NO_RANGE" 0247 // SERVER_PRECONDITION 0248 {QStringLiteral("SERVER_BAD_CONTENT"), i18n("The server does not have the requested data.")}, 0249 0250 {QStringLiteral("CRASH"), i18n("The browser application closed unexpectedly.")}, 0251 }; 0252 0253 const QString &errorValue = errors.value(error); 0254 if (errorValue.isEmpty()) { // unknown error 0255 setError(KIO::ERR_UNKNOWN); 0256 setErrorText(i18n("An unknown error occurred while downloading.")); 0257 emitResult(); 0258 return; 0259 } 0260 0261 // KIO::Error doesn't have a UserDefined one, let's just use magic numbers then 0262 // TODO at least set the KIO::Errors that we do have 0263 setError(1000); 0264 setErrorText(errorValue); 0265 0266 emitResult(); 0267 return; 0268 } 0269 0270 it = payload.constFind(QStringLiteral("state")); 0271 if (it != end) { 0272 const QString state = it->toString(); 0273 0274 // We ignore "interrupted" state and only cancel if we get supplied an "error" 0275 if (state == QLatin1String("complete")) { 0276 setError(KJob::NoError); 0277 setProcessedAmount(KJob::Files, 1); 0278 0279 // Add to recent document 0280 addToRecentDocuments(); 0281 0282 // Write origin url into extended file attributes 0283 saveOriginUrl(); 0284 0285 emitResult(); 0286 return; 0287 } 0288 } 0289 } 0290 0291 void DownloadJob::updateDescription() 0292 { 0293 Q_EMIT description(this, 0294 i18nc("Job heading, like 'Copying'", "Downloading"), 0295 qMakePair<QString, QString>(i18nc("The URL being downloaded", "Source"), originUrl().toDisplayString()), 0296 qMakePair<QString, QString>(i18nc("The location being downloaded to", "Destination"), m_destination.toLocalFile())); 0297 } 0298 0299 void DownloadJob::addToRecentDocuments() 0300 { 0301 if (m_incognito || m_fileName.isEmpty()) { 0302 return; 0303 } 0304 0305 const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads")); 0306 0307 const bool enabled = settings.value(QStringLiteral("addToRecentDocuments")).toBool(); 0308 if (!enabled) { 0309 return; 0310 } 0311 0312 KActivities::ResourceInstance::notifyAccessed(QUrl::fromLocalFile(m_fileName), qApp->desktopFileName()); 0313 } 0314 0315 void DownloadJob::saveOriginUrl() 0316 { 0317 QUrl url = originUrl(); 0318 0319 if (m_incognito 0320 || !url.isValid() 0321 // Blob URLs are dynamically created through JavaScript and cannot be accessed from the outside 0322 || url.scheme() == QLatin1String("blob") 0323 // Data URLs contain the actual data of the file we just downloaded anyway 0324 || url.scheme() == QLatin1String("data")) { 0325 return; 0326 } 0327 0328 const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads")); 0329 0330 const bool saveOriginUrl = settings.value(QStringLiteral("saveOriginUrl")).toBool(); 0331 if (!saveOriginUrl) { 0332 return; 0333 } 0334 0335 KFileMetaData::UserMetaData md(m_fileName); 0336 0337 url.setPassword(QString()); 0338 0339 md.setOriginUrl(url); 0340 }