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 }