File indexing completed on 2024-04-28 15:28:54

0001 /*
0002     SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
0003     SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "cache.h"
0009 
0010 #include <QDir>
0011 #include <QFile>
0012 #include <QFileInfo>
0013 #include <QFileSystemWatcher>
0014 #include <QPointer>
0015 #include <QTimer>
0016 #include <QXmlStreamReader>
0017 #include <knewstuffcore_debug.h>
0018 #include <qstandardpaths.h>
0019 
0020 class KNSCore::CachePrivate
0021 {
0022 public:
0023     CachePrivate(Cache *qq)
0024         : q(qq)
0025     {
0026     }
0027     ~CachePrivate()
0028     {
0029     }
0030 
0031     Cache *q;
0032     QHash<QString, EntryInternal::List> requestCache;
0033 
0034     QPointer<QTimer> throttleTimer;
0035     void throttleWrite()
0036     {
0037         if (!throttleTimer) {
0038             throttleTimer = new QTimer(q);
0039             QObject::connect(throttleTimer, &QTimer::timeout, q, [this]() {
0040                 q->writeRegistry();
0041             });
0042             throttleTimer->setSingleShot(true);
0043             throttleTimer->setInterval(1000);
0044         }
0045         throttleTimer->start();
0046     }
0047 };
0048 
0049 using namespace KNSCore;
0050 
0051 typedef QHash<QString, QWeakPointer<Cache>> CacheHash;
0052 Q_GLOBAL_STATIC(CacheHash, s_caches)
0053 Q_GLOBAL_STATIC(QFileSystemWatcher, s_watcher)
0054 
0055 Cache::Cache(const QString &appName)
0056     : QObject(nullptr)
0057     , m_kns2ComponentName(appName)
0058     , d(new CachePrivate(this))
0059 {
0060     const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/knewstuff3/");
0061     QDir().mkpath(path);
0062     registryFile = path + appName + QStringLiteral(".knsregistry");
0063     qCDebug(KNEWSTUFFCORE) << "Using registry file: " << registryFile;
0064     setProperty("dirty", false); // KF6 make normal variable
0065 
0066     s_watcher->addPath(registryFile);
0067 
0068     std::function<void()> changeChecker = [this, &changeChecker]() {
0069         if (property("writingRegistry").toBool()) {
0070             QTimer::singleShot(0, this, changeChecker);
0071         } else {
0072             setProperty("reloadingRegistry", true);
0073             const QSet<KNSCore::EntryInternal> oldCache = cache;
0074             cache.clear();
0075             readRegistry();
0076             // First run through the old cache and see if any have disappeared (at
0077             // which point we need to set them as available and emit that change)
0078             for (const EntryInternal &entry : oldCache) {
0079                 if (!cache.contains(entry)) {
0080                     EntryInternal removedEntry(entry);
0081                     removedEntry.setStatus(KNS3::Entry::Deleted);
0082                     Q_EMIT entryChanged(removedEntry);
0083                 }
0084             }
0085             // Then run through the new cache and see if there's any that were not
0086             // in the old cache (at which point just emit those as having changed,
0087             // they're already the correct status)
0088             for (const EntryInternal &entry : cache) {
0089                 auto iterator = oldCache.constFind(entry);
0090                 if (iterator == oldCache.constEnd()) {
0091                     Q_EMIT entryChanged(entry);
0092                 } else if ((*iterator).status() != entry.status()) {
0093                     // If there are entries which are in both, but which have changed their
0094                     // status, we should adopt the status from the newly loaded cache in place
0095                     // of the one in the old cache. In reality, what this means is we just
0096                     // need to emit the changed signal for anything in the new cache which
0097                     // doesn't match the old one
0098                     Q_EMIT entryChanged(entry);
0099                 }
0100             }
0101             setProperty("reloadingRegistry", false);
0102         }
0103     };
0104     connect(&*s_watcher, &QFileSystemWatcher::fileChanged, this, [this, changeChecker](const QString &file) {
0105         if (file == registryFile) {
0106             changeChecker();
0107         }
0108     });
0109 }
0110 
0111 QSharedPointer<Cache> Cache::getCache(const QString &appName)
0112 {
0113     CacheHash::const_iterator it = s_caches()->constFind(appName);
0114     if ((it != s_caches()->constEnd()) && !(*it).isNull()) {
0115         return QSharedPointer<Cache>(*it);
0116     }
0117 
0118     QSharedPointer<Cache> p(new Cache(appName));
0119     s_caches()->insert(appName, QWeakPointer<Cache>(p));
0120     QObject::connect(p.data(), &QObject::destroyed, [appName] {
0121         if (auto cache = s_caches()) {
0122             cache->remove(appName);
0123         }
0124     });
0125 
0126     return p;
0127 }
0128 
0129 Cache::~Cache()
0130 {
0131     s_watcher->removePath(registryFile);
0132 }
0133 
0134 void Cache::readRegistry()
0135 {
0136 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
0137     // read KNS2 registry first to migrate it
0138     readKns2MetaFiles();
0139 #endif
0140 
0141     QFile f(registryFile);
0142     if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
0143         if (QFileInfo::exists(registryFile)) {
0144             qWarning() << "The file " << registryFile << " could not be opened.";
0145         }
0146         return;
0147     }
0148 
0149     QXmlStreamReader reader(&f);
0150     if (reader.hasError() || !reader.readNextStartElement()) {
0151         qCWarning(KNEWSTUFFCORE) << "The file could not be parsed.";
0152         return;
0153     }
0154 
0155     if (reader.name() != QLatin1String("hotnewstuffregistry")) {
0156         qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest.";
0157         return;
0158     }
0159 
0160     for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) {
0161         if (token != QXmlStreamReader::StartElement) {
0162             continue;
0163         }
0164         EntryInternal e;
0165         e.setEntryXML(reader);
0166         e.setSource(EntryInternal::Cache);
0167         cache.insert(e);
0168         Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement);
0169     }
0170 
0171     qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << cache.size();
0172 }
0173 
0174 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
0175 void Cache::readKns2MetaFiles()
0176 {
0177     qCDebug(KNEWSTUFFCORE) << "Loading KNS2 registry of files for the component: " << m_kns2ComponentName;
0178 
0179     const auto realAppName = m_kns2ComponentName.split(QLatin1Char(':'))[0];
0180 
0181     const QStringList dirs =
0182         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knewstuff2-entries.registry"), QStandardPaths::LocateDirectory);
0183     for (QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) {
0184         qCDebug(KNEWSTUFFCORE) << QStringLiteral(" + Load from directory '") + (*it) + QStringLiteral("'.");
0185         QDir dir((*it));
0186         const QStringList files = dir.entryList(QDir::Files | QDir::Readable);
0187         for (QStringList::const_iterator fit = files.begin(); fit != files.end(); ++fit) {
0188             QString filepath = (*it) + QLatin1Char('/') + (*fit);
0189 
0190             qCDebug(KNEWSTUFFCORE) << QStringLiteral(" Load from file '") + filepath + QStringLiteral("'.");
0191 
0192             QFileInfo info(filepath);
0193             QFile f(filepath);
0194 
0195             // first see if this file is even for this app
0196             // because the registry contains entries for all apps
0197             // FIXMEE: should be able to do this with a filter on the entryList above probably
0198             QString thisAppName = QString::fromUtf8(QByteArray::fromBase64(info.baseName().toUtf8()));
0199 
0200             // NOTE: the ":" needs to always coincide with the separator character used in
0201             // the id(Entry*) method
0202             thisAppName = thisAppName.split(QLatin1Char(':'))[0];
0203 
0204             if (thisAppName != realAppName) {
0205                 continue;
0206             }
0207 
0208             if (!f.open(QIODevice::ReadOnly)) {
0209                 qWarning() << "The file: " << filepath << " could not be opened.";
0210                 continue;
0211             }
0212 
0213             QDomDocument doc;
0214             if (!doc.setContent(&f)) {
0215                 qWarning() << "The file could not be parsed.";
0216                 return;
0217             }
0218             qCDebug(KNEWSTUFFCORE) << "found entry: " << doc.toString();
0219 
0220             QDomElement root = doc.documentElement();
0221             if (root.tagName() != QLatin1String("ghnsinstall")) {
0222                 qWarning() << "The file doesn't seem to be of interest.";
0223                 return;
0224             }
0225 
0226             // The .meta files only contain one entry
0227             QDomElement stuff = root.firstChildElement(QStringLiteral("stuff"));
0228             EntryInternal e;
0229             e.setEntryXML(stuff);
0230             e.setSource(EntryInternal::Cache);
0231 
0232             if (e.payload().startsWith(QLatin1String("http://download.kde.org/khotnewstuff"))) {
0233                 // This is 99% sure a opendesktop file, make it a real one.
0234                 e.setProviderId(QStringLiteral("https://api.opendesktop.org/v1/"));
0235                 e.setHomepage(QUrl(QString(QLatin1String("http://opendesktop.org/content/show.php?content=") + e.uniqueId())));
0236 
0237             } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml/"))) {
0238                 // kvmtl-1
0239                 e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml/kvtml.xml"));
0240             } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml2/"))) {
0241                 // kvmtl-2
0242                 e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml2/provider41.xml"));
0243             } else {
0244                 // we failed, skip
0245                 qWarning() << "Could not load entry: " << filepath;
0246                 continue;
0247             }
0248 
0249             e.setStatus(KNS3::Entry::Installed);
0250 
0251             cache.insert(e);
0252             QDomDocument tmp(QStringLiteral("yay"));
0253             tmp.appendChild(e.entryXML());
0254             qCDebug(KNEWSTUFFCORE) << "new entry: " << tmp.toString();
0255 
0256             f.close();
0257 
0258             QDir dir;
0259             if (!dir.remove(filepath)) {
0260                 qWarning() << "could not delete old kns2 .meta file: " << filepath;
0261             } else {
0262                 qCDebug(KNEWSTUFFCORE) << "Migrated KNS2 entry to KNS3.";
0263             }
0264         }
0265     }
0266     setProperty("dirty", false);
0267 }
0268 #endif
0269 
0270 EntryInternal::List Cache::registryForProvider(const QString &providerId)
0271 {
0272     EntryInternal::List entries;
0273     for (const EntryInternal &e : std::as_const(cache)) {
0274         if (e.providerId() == providerId) {
0275             entries.append(e);
0276         }
0277     }
0278     return entries;
0279 }
0280 
0281 EntryInternal::List Cache::registry() const
0282 {
0283     EntryInternal::List entries;
0284     for (const EntryInternal &e : std::as_const(cache)) {
0285         entries.append(e);
0286     }
0287     return entries;
0288 }
0289 
0290 void Cache::writeRegistry()
0291 {
0292     if (!property("dirty").toBool()) {
0293         return;
0294     }
0295 
0296     qCDebug(KNEWSTUFFCORE) << "Write registry";
0297 
0298     setProperty("writingRegistry", true);
0299     QFile f(registryFile);
0300     if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
0301         qWarning() << "Cannot write meta information to '" << registryFile << "'.";
0302         return;
0303     }
0304 
0305     QDomDocument doc(QStringLiteral("khotnewstuff3"));
0306     doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
0307     QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry"));
0308     doc.appendChild(root);
0309 
0310     for (const EntryInternal &entry : std::as_const(cache)) {
0311         // Write the entry, unless the policy is CacheNever and the entry is not installed.
0312         if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) {
0313             QDomElement exml = entry.entryXML();
0314             root.appendChild(exml);
0315         }
0316     }
0317 
0318     QTextStream metastream(&f);
0319     metastream << doc.toByteArray();
0320 
0321     setProperty("dirty", false);
0322     setProperty("writingRegistry", false);
0323 }
0324 
0325 void Cache::registerChangedEntry(const KNSCore::EntryInternal &entry)
0326 {
0327     // If we have intermediate states, like updating or installing we do not want to write them
0328     if (entry.status() == KNS3::Entry::Updating || entry.status() == KNS3::Entry::Installing) {
0329         return;
0330     }
0331     if (!property("reloadingRegistry").toBool()) {
0332         setProperty("dirty", true);
0333         cache.remove(entry); // If value already exists in the set, the set is left unchanged
0334         cache.insert(entry);
0335         d->throttleWrite();
0336     }
0337 }
0338 
0339 void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::EntryInternal::List &entries)
0340 {
0341     // append new entries
0342     auto &cacheList = d->requestCache[request.hashForRequest()];
0343     for (const auto &entry : entries) {
0344         if (!cacheList.contains(entry)) {
0345             cacheList.append(entry);
0346         }
0347     }
0348     qCDebug(KNEWSTUFFCORE) << request.hashForRequest() << " add: " << entries.size() << " keys: " << d->requestCache.keys();
0349 }
0350 
0351 EntryInternal::List Cache::requestFromCache(const KNSCore::Provider::SearchRequest &request)
0352 {
0353     qCDebug(KNEWSTUFFCORE) << request.hashForRequest();
0354     return d->requestCache.value(request.hashForRequest());
0355 }
0356 
0357 void KNSCore::Cache::removeDeletedEntries()
0358 {
0359     QMutableSetIterator<KNSCore::EntryInternal> i(cache);
0360     while (i.hasNext()) {
0361         const KNSCore::EntryInternal &entry = i.next();
0362         bool installedFileExists{false};
0363         const QStringList installedFiles = entry.installedFiles();
0364         for (const auto &installedFile : installedFiles) {
0365             // Handle the /* notation, BUG: 425704
0366             if (installedFile.endsWith(QLatin1String("/*"))) {
0367                 if (QDir(installedFile.left(installedFile.size() - 2)).exists()) {
0368                     installedFileExists = true;
0369                     break;
0370                 }
0371             } else if (QFile::exists(installedFile)) {
0372                 installedFileExists = true;
0373                 break;
0374             }
0375         }
0376         if (!installedFileExists) {
0377             i.remove();
0378             setProperty("dirty", true);
0379         }
0380     }
0381     writeRegistry();
0382 }
0383 
0384 KNSCore::EntryInternal KNSCore::Cache::entryFromInstalledFile(const QString &installedFile) const
0385 {
0386     for (const EntryInternal &entry : cache) {
0387         if (entry.installedFiles().contains(installedFile)) {
0388             return entry;
0389         }
0390     }
0391     return EntryInternal{};
0392 }
0393 
0394 #include "moc_cache.cpp"