| 1 | /* |
| 2 | This file is part of the KDE libraries |
| 3 | SPDX-FileCopyrightText: 2016 David Faure <faure@kde.org> |
| 4 | SPDX-FileCopyrightText: 2001 Malte Starostik <malte@kde.org> |
| 5 | |
| 6 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 7 | */ |
| 8 | |
| 9 | #include "faviconrequestjob.h" |
| 10 | #include <faviconscache_p.h> |
| 11 | |
| 12 | #include "favicons_debug.h" |
| 13 | |
| 14 | #include <KConfig> |
| 15 | #include <KIO/TransferJob> |
| 16 | #include <KLocalizedString> |
| 17 | |
| 18 | #include <QBuffer> |
| 19 | #include <QCache> |
| 20 | #include <QDate> |
| 21 | #include <QFileInfo> |
| 22 | #include <QImage> |
| 23 | #include <QImageReader> |
| 24 | #include <QSaveFile> |
| 25 | #include <QStandardPaths> |
| 26 | #include <QUrl> |
| 27 | |
| 28 | using namespace KIO; |
| 29 | |
| 30 | static bool isIconOld(const QString &icon) |
| 31 | { |
| 32 | const QFileInfo info(icon); |
| 33 | if (!info.exists()) { |
| 34 | qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "yes, no such file" ; |
| 35 | return true; // Trigger a new download on error |
| 36 | } |
| 37 | const QDate date = info.lastModified().date(); |
| 38 | |
| 39 | qCDebug(FAVICONS_LOG) << "isIconOld" << icon << "?" ; |
| 40 | return date.daysTo(d: QDate::currentDate()) > 7; // arbitrary value (one week) |
| 41 | } |
| 42 | |
| 43 | class KIO::FavIconRequestJobPrivate |
| 44 | { |
| 45 | public: |
| 46 | FavIconRequestJobPrivate(const QUrl &hostUrl, KIO::LoadType reload) |
| 47 | : m_hostUrl(hostUrl) |
| 48 | , m_reload(reload) |
| 49 | { |
| 50 | } |
| 51 | |
| 52 | // slots |
| 53 | void slotData(KIO::Job *job, const QByteArray &data); |
| 54 | |
| 55 | QUrl m_hostUrl; |
| 56 | QUrl m_iconUrl; |
| 57 | QString m_iconFile; |
| 58 | QByteArray m_iconData; |
| 59 | KIO::LoadType m_reload; |
| 60 | }; |
| 61 | |
| 62 | FavIconRequestJob::FavIconRequestJob(const QUrl &hostUrl, LoadType reload, QObject *parent) |
| 63 | : KCompositeJob(parent) |
| 64 | , d(new FavIconRequestJobPrivate(hostUrl, reload)) |
| 65 | { |
| 66 | QMetaObject::invokeMethod(object: this, function: &FavIconRequestJob::doStart, type: Qt::QueuedConnection); |
| 67 | } |
| 68 | |
| 69 | FavIconRequestJob::~FavIconRequestJob() = default; |
| 70 | |
| 71 | void FavIconRequestJob::setIconUrl(const QUrl &iconUrl) |
| 72 | { |
| 73 | d->m_iconUrl = iconUrl; |
| 74 | } |
| 75 | |
| 76 | QString FavIconRequestJob::iconFile() const |
| 77 | { |
| 78 | return d->m_iconFile; |
| 79 | } |
| 80 | |
| 81 | QUrl FavIconRequestJob::hostUrl() const |
| 82 | { |
| 83 | return d->m_hostUrl; |
| 84 | } |
| 85 | |
| 86 | void FavIconRequestJob::doStart() |
| 87 | { |
| 88 | KIO::FavIconsCache *cache = KIO::FavIconsCache::instance(); |
| 89 | QUrl iconUrl = d->m_iconUrl; |
| 90 | const bool isNewIconUrl = !iconUrl.isEmpty(); |
| 91 | if (isNewIconUrl) { |
| 92 | cache->setIconForUrl(url: d->m_hostUrl, iconUrl: d->m_iconUrl); |
| 93 | } else { |
| 94 | iconUrl = cache->iconUrlForUrl(url: d->m_hostUrl); |
| 95 | } |
| 96 | if (d->m_reload == NoReload) { |
| 97 | const QString iconFile = cache->cachePathForIconUrl(iconUrl); |
| 98 | if (!isIconOld(icon: iconFile)) { |
| 99 | qCDebug(FAVICONS_LOG) << "existing icon not old, reload not requested -> doing nothing" ; |
| 100 | d->m_iconFile = iconFile; |
| 101 | emitResult(); |
| 102 | return; |
| 103 | } |
| 104 | |
| 105 | if (cache->isFailedDownload(url: iconUrl)) { |
| 106 | qCDebug(FAVICONS_LOG) << iconUrl << "already in failedDownloads, emitting error" ; |
| 107 | setError(KIO::ERR_DOES_NOT_EXIST); |
| 108 | setErrorText(i18n("No favicon found for %1" , d->m_hostUrl.host())); |
| 109 | emitResult(); |
| 110 | return; |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | qCDebug(FAVICONS_LOG) << "downloading" << iconUrl; |
| 115 | KIO::TransferJob *job = KIO::get(url: iconUrl, reload: d->m_reload, flags: KIO::HideProgressInfo); |
| 116 | QMap<QString, QString> metaData; |
| 117 | metaData.insert(QStringLiteral("ssl_no_client_cert" ), QStringLiteral("true" )); |
| 118 | metaData.insert(QStringLiteral("ssl_no_ui" ), QStringLiteral("true" )); |
| 119 | metaData.insert(QStringLiteral("UseCache" ), QStringLiteral("false" )); |
| 120 | metaData.insert(QStringLiteral("cookies" ), QStringLiteral("none" )); |
| 121 | metaData.insert(QStringLiteral("no-www-auth" ), QStringLiteral("true" )); |
| 122 | job->addMetaData(values: metaData); |
| 123 | QObject::connect(sender: job, signal: &KIO::TransferJob::data, context: this, slot: [this](KIO::Job *job, const QByteArray &data) { |
| 124 | d->slotData(job, data); |
| 125 | }); |
| 126 | addSubjob(job); |
| 127 | } |
| 128 | |
| 129 | void FavIconRequestJob::slotResult(KJob *job) |
| 130 | { |
| 131 | KIO::TransferJob *tjob = static_cast<KIO::TransferJob *>(job); |
| 132 | const QUrl &iconUrl = tjob->url(); |
| 133 | KIO::FavIconsCache *cache = KIO::FavIconsCache::instance(); |
| 134 | if (!job->error()) { |
| 135 | QBuffer buffer(&d->m_iconData); |
| 136 | buffer.open(openMode: QIODevice::ReadOnly); |
| 137 | QImageReader ir(&buffer); |
| 138 | QSize desired(16, 16); |
| 139 | if (ir.canRead()) { |
| 140 | while (ir.imageCount() > 1 && ir.currentImageRect() != QRect(0, 0, desired.width(), desired.height())) { |
| 141 | if (!ir.jumpToNextImage()) { |
| 142 | break; |
| 143 | } |
| 144 | } |
| 145 | ir.setScaledSize(desired); |
| 146 | const QImage img = ir.read(); |
| 147 | if (!img.isNull()) { |
| 148 | cache->ensureCacheExists(); |
| 149 | const QString localPath = cache->cachePathForIconUrl(iconUrl); |
| 150 | qCDebug(FAVICONS_LOG) << "Saving image to" << localPath; |
| 151 | QSaveFile saveFile(localPath); |
| 152 | if (saveFile.open(flags: QIODevice::WriteOnly) && img.save(device: &saveFile, format: "PNG" ) && saveFile.commit()) { |
| 153 | d->m_iconFile = localPath; |
| 154 | } else { |
| 155 | setError(KIO::ERR_CANNOT_WRITE); |
| 156 | setErrorText(i18n("Error saving image to %1" , localPath)); |
| 157 | } |
| 158 | } else { |
| 159 | qCDebug(FAVICONS_LOG) << "QImageReader read() returned a null image" ; |
| 160 | } |
| 161 | } else { |
| 162 | qCDebug(FAVICONS_LOG) << "QImageReader canRead returned false" ; |
| 163 | } |
| 164 | } else if (job->error() == KJob::KilledJobError) { // we killed it in slotData |
| 165 | setError(KIO::ERR_WORKER_DEFINED); |
| 166 | setErrorText(i18n("Icon file too big, download aborted" )); |
| 167 | } else { |
| 168 | setError(job->error()); |
| 169 | setErrorText(job->errorString()); // not errorText(), because "this" is a KJob, with no errorString building logic |
| 170 | } |
| 171 | d->m_iconData.clear(); // release memory |
| 172 | if (d->m_iconFile.isEmpty()) { |
| 173 | qCDebug(FAVICONS_LOG) << "adding" << iconUrl << "to failed downloads due to error:" << errorString(); |
| 174 | cache->addFailedDownload(url: iconUrl); |
| 175 | } else { |
| 176 | cache->removeFailedDownload(url: iconUrl); |
| 177 | } |
| 178 | KCompositeJob::removeSubjob(job); |
| 179 | emitResult(); |
| 180 | } |
| 181 | |
| 182 | void FavIconRequestJobPrivate::slotData(Job *job, const QByteArray &data) |
| 183 | { |
| 184 | KIO::TransferJob *tjob = static_cast<KIO::TransferJob *>(job); |
| 185 | unsigned int oldSize = m_iconData.size(); |
| 186 | // Size limit. Stop downloading if the file is huge. |
| 187 | // Testcase (as of june 2008, at least): http://planet-soc.com/favicon.ico, 136K and strange format. |
| 188 | // Another case: sites which redirect from "/favicon.ico" to "/" and return the main page. |
| 189 | if (oldSize > 0x10000) { // 65K |
| 190 | qCDebug(FAVICONS_LOG) << "Favicon too big, aborting download of" << tjob->url(); |
| 191 | const QUrl iconUrl = tjob->url(); |
| 192 | KIO::FavIconsCache::instance()->addFailedDownload(url: iconUrl); |
| 193 | tjob->kill(verbosity: KJob::EmitResult); |
| 194 | } else { |
| 195 | m_iconData.resize(size: oldSize + data.size()); |
| 196 | memcpy(dest: m_iconData.data() + oldSize, src: data.data(), n: data.size()); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | #include "moc_faviconrequestjob.cpp" |
| 201 | |