File indexing completed on 2024-05-12 16:21:30

0001 /**
0002  * SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
0003  * SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006  */
0007 
0008 #include "fetcher.h"
0009 #include "fetcherlogging.h"
0010 
0011 #include <KLocalizedString>
0012 #include <QDateTime>
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QFile>
0016 #include <QFileInfo>
0017 #include <QNetworkAccessManager>
0018 #include <QNetworkProxyFactory>
0019 #include <QNetworkReply>
0020 #include <QTime>
0021 #include <QTimer>
0022 
0023 #include "database.h"
0024 #include "enclosure.h"
0025 #include "fetchfeedsjob.h"
0026 #include "kasts-version.h"
0027 #include "models/errorlogmodel.h"
0028 #include "networkconnectionmanager.h"
0029 #include "settingsmanager.h"
0030 #include "storagemanager.h"
0031 #include "sync/sync.h"
0032 
0033 Fetcher::Fetcher()
0034 {
0035     connect(this, &Fetcher::error, &ErrorLogModel::instance(), &ErrorLogModel::monitorErrorMessages);
0036 
0037     m_updateProgress = -1;
0038     m_updateTotal = -1;
0039     m_updating = false;
0040 
0041     manager = new QNetworkAccessManager(this);
0042     manager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0043     manager->setStrictTransportSecurityEnabled(true);
0044     manager->enableStrictTransportSecurityStore(true);
0045 
0046     QNetworkProxyFactory::setUseSystemConfiguration(true);
0047 }
0048 
0049 void Fetcher::fetch(const QString &url)
0050 {
0051     QStringList urls(url);
0052     fetch(urls);
0053 }
0054 
0055 void Fetcher::fetchAll()
0056 {
0057     if (Sync::instance().syncEnabled() && SettingsManager::self()->syncWhenUpdatingFeeds()) {
0058         Sync::instance().doRegularSync(true);
0059     } else {
0060         QStringList urls;
0061         QSqlQuery query;
0062         query.prepare(QStringLiteral("SELECT url FROM Feeds;"));
0063         Database::instance().execute(query);
0064         while (query.next()) {
0065             urls += query.value(0).toString();
0066         }
0067 
0068         if (urls.count() > 0) {
0069             fetch(urls);
0070         }
0071     }
0072 }
0073 
0074 void Fetcher::fetch(const QStringList &urls)
0075 {
0076     if (m_updating)
0077         return; // update is already running, do nothing
0078 
0079     m_updating = true;
0080     m_updateProgress = 0;
0081     m_updateTotal = urls.count();
0082     Q_EMIT updatingChanged(m_updating);
0083     Q_EMIT updateProgressChanged(m_updateProgress);
0084     Q_EMIT updateTotalChanged(m_updateTotal);
0085 
0086     qCDebug(kastsFetcher) << "Create fetchFeedsJob";
0087     FetchFeedsJob *fetchFeedsJob = new FetchFeedsJob(urls, this);
0088     connect(this, &Fetcher::cancelFetching, fetchFeedsJob, &FetchFeedsJob::abort);
0089     connect(fetchFeedsJob, &FetchFeedsJob::processedAmountChanged, this, [this](KJob *job, KJob::Unit unit, qulonglong amount) {
0090         qCDebug(kastsFetcher) << "FetchFeedsJob::processedAmountChanged:" << amount;
0091         Q_UNUSED(job);
0092         Q_ASSERT(unit == KJob::Unit::Items);
0093         m_updateProgress = amount;
0094         Q_EMIT updateProgressChanged(m_updateProgress);
0095     });
0096     connect(fetchFeedsJob, &FetchFeedsJob::result, this, [this, fetchFeedsJob]() {
0097         qCDebug(kastsFetcher) << "result slot of FetchFeedsJob";
0098         if (fetchFeedsJob->error() && !fetchFeedsJob->aborted()) {
0099             Q_EMIT error(Error::Type::FeedUpdate, QString(), QString(), fetchFeedsJob->error(), fetchFeedsJob->errorString(), QString());
0100         }
0101         if (m_updating) {
0102             m_updating = false;
0103             Q_EMIT updatingChanged(m_updating);
0104         }
0105     });
0106 
0107     fetchFeedsJob->start();
0108     qCDebug(kastsFetcher) << "end of Fetcher::fetch";
0109 }
0110 
0111 QString Fetcher::image(const QString &url)
0112 {
0113     if (url.isEmpty()) {
0114         return QLatin1String("no-image");
0115     }
0116 
0117     // if image is already cached, then return the path
0118     QString path = StorageManager::instance().imagePath(url);
0119     if (QFileInfo::exists(path)) {
0120         if (QFileInfo(path).size() != 0) {
0121             return QUrl::fromLocalFile(path).toString();
0122         }
0123     }
0124 
0125     // avoid restarting an image download if it's already running
0126     if (m_ongoingImageDownloads.contains(url)) {
0127         return QLatin1String("fetching");
0128     }
0129 
0130     // if image has not yet been cached, then check for network connectivity if
0131     // possible; and download the image
0132     if (!NetworkConnectionManager::instance().imageDownloadsAllowed()) {
0133         return QLatin1String("no-image");
0134     }
0135 
0136     m_ongoingImageDownloads.insert(url);
0137     QNetworkRequest request((QUrl(url)));
0138     request.setTransferTimeout();
0139     QNetworkReply *reply = get(request);
0140     connect(reply, &QNetworkReply::finished, this, [=]() {
0141         if (reply->isOpen() && !reply->error()) {
0142             QByteArray data = reply->readAll();
0143             QFile file(path);
0144             file.open(QIODevice::WriteOnly);
0145             file.write(data);
0146             file.close();
0147             Q_EMIT downloadFinished(url);
0148         }
0149         m_ongoingImageDownloads.remove(url);
0150         reply->deleteLater();
0151     });
0152     return QLatin1String("fetching");
0153 }
0154 
0155 QNetworkReply *Fetcher::download(const QString &url, const QString &filePath) const
0156 {
0157     QNetworkRequest request((QUrl(url)));
0158     request.setTransferTimeout();
0159 
0160     QFile *file = new QFile(filePath);
0161     if (file->exists() && file->size() > 0) {
0162         // try to resume download
0163         int resumedAt = file->size();
0164         qCDebug(kastsFetcher) << "Resuming download at" << resumedAt << "bytes";
0165         QByteArray rangeHeaderValue = QByteArray("bytes=") + QByteArray::number(resumedAt) + QByteArray("-");
0166         request.setRawHeader(QByteArray("Range"), rangeHeaderValue);
0167         file->open(QIODevice::WriteOnly | QIODevice::Append);
0168     } else {
0169         qCDebug(kastsFetcher) << "Starting new download";
0170         file->open(QIODevice::WriteOnly);
0171     }
0172 
0173     QNetworkReply *reply = get(request);
0174 
0175     connect(reply, &QNetworkReply::readyRead, this, [=]() {
0176         if (reply->isOpen() && file) {
0177             QByteArray data = reply->readAll();
0178             file->write(data);
0179         }
0180     });
0181 
0182     connect(reply, &QNetworkReply::finished, this, [=]() {
0183         if (reply->isOpen() && file) {
0184             QByteArray data = reply->readAll();
0185             file->write(data);
0186             file->close();
0187 
0188             Q_EMIT downloadFinished(url);
0189         }
0190 
0191         // clean up; close file if still open in case something has gone wrong
0192         if (file) {
0193             if (file->isOpen()) {
0194                 file->close();
0195             }
0196             delete file;
0197         }
0198         reply->deleteLater();
0199     });
0200 
0201     return reply;
0202 }
0203 
0204 void Fetcher::getRedirectedUrl(const QUrl &url)
0205 {
0206     QNetworkRequest request((QUrl(url)));
0207     request.setTransferTimeout(5000); // wait 5 seconds; it will fall back to original url otherwise
0208 
0209     QNetworkReply *reply = head(request);
0210 
0211     connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
0212         qCDebug(kastsFetcher) << "finished looking for redirect; this is the old url and the redirected url:" << url << reply->url();
0213 
0214         QUrl newUrl = reply->url();
0215         QTimer::singleShot(0, this, [this, url, newUrl]() {
0216             Q_EMIT foundRedirectedUrl(url, newUrl);
0217         });
0218         reply->deleteLater();
0219     });
0220 }
0221 
0222 QNetworkReply *Fetcher::get(QNetworkRequest &request) const
0223 {
0224     setHeader(request);
0225     return manager->get(request);
0226 }
0227 
0228 QNetworkReply *Fetcher::post(QNetworkRequest &request, const QByteArray &data) const
0229 {
0230     setHeader(request);
0231     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
0232     return manager->post(request, data);
0233 }
0234 
0235 QNetworkReply *Fetcher::head(QNetworkRequest &request) const
0236 {
0237     setHeader(request);
0238     return manager->head(request);
0239 }
0240 
0241 void Fetcher::setHeader(QNetworkRequest &request) const
0242 {
0243     request.setRawHeader(QByteArray("User-Agent"), QByteArray("Kasts/") + QByteArray(KASTS_VERSION_STRING) + QByteArray(" Syndication"));
0244 }