File indexing completed on 2024-04-14 03:54:35

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 1999, 2007 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 1999 Waldo Bastian <bastian@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #include "kbuildmimetypefactory_p.h"
0010 #include "kbuildservicefactory_p.h"
0011 #include "kbuildservicegroupfactory_p.h"
0012 #include "ksycoca.h"
0013 
0014 #include "ksycocadict_p.h"
0015 #include "sycocadebug.h"
0016 #include <KDesktopFile>
0017 
0018 #include <QDebug>
0019 #include <QDir>
0020 #include <QMimeDatabase>
0021 
0022 #include <QStandardPaths>
0023 #include <kmimetypefactory_p.h>
0024 
0025 KBuildServiceFactory::KBuildServiceFactory(KBuildMimeTypeFactory *mimeTypeFactory, KBuildServiceGroupFactory *serviceGroupFactory)
0026     : KServiceFactory(mimeTypeFactory->sycoca())
0027     , m_nameMemoryHash()
0028     , m_relNameMemoryHash()
0029     , m_menuIdMemoryHash()
0030     , m_dupeDict()
0031     , m_mimeTypeFactory(mimeTypeFactory)
0032     , m_serviceGroupFactory(serviceGroupFactory)
0033 {
0034     m_nameDict = new KSycocaDict();
0035     m_relNameDict = new KSycocaDict();
0036     m_menuIdDict = new KSycocaDict();
0037 }
0038 
0039 KBuildServiceFactory::~KBuildServiceFactory()
0040 {
0041 }
0042 
0043 KService::Ptr KBuildServiceFactory::findServiceByDesktopName(const QString &name)
0044 {
0045     return m_nameMemoryHash.value(name);
0046 }
0047 
0048 KService::Ptr KBuildServiceFactory::findServiceByDesktopPath(const QString &name)
0049 {
0050     return m_relNameMemoryHash.value(name);
0051 }
0052 
0053 KService::Ptr KBuildServiceFactory::findServiceByMenuId(const QString &menuId)
0054 {
0055     return m_menuIdMemoryHash.value(menuId);
0056 }
0057 
0058 KSycocaEntry *KBuildServiceFactory::createEntry(const QString &file) const
0059 {
0060     const QStringView name = QStringView(file).mid(file.lastIndexOf(QLatin1Char('/')) + 1);
0061 
0062     // Is it a .desktop file?
0063     if (name.endsWith(QLatin1String(".desktop"))) {
0064         // qCDebug(SYCOCA) << file;
0065 
0066         Q_ASSERT(QDir::isAbsolutePath(file));
0067         KService *serv = new KService(file);
0068 
0069         // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath();
0070         // Note that the menuId will be set by the vfolder_menu.cpp code just after
0071         // createEntry returns.
0072 
0073         if (serv->isValid() && !serv->isDeleted()) {
0074             // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath() << "storageId=" << serv->storageId();
0075             return serv;
0076         } else {
0077             if (!serv->isDeleted()) {
0078                 qCWarning(SYCOCA) << "Invalid Service : " << file;
0079             }
0080             delete serv;
0081             return nullptr;
0082         }
0083     } // TODO else if a Windows application,  new KService(name, exec, icon)
0084     return nullptr;
0085 }
0086 
0087 void KBuildServiceFactory::saveHeader(QDataStream &str)
0088 {
0089     KSycocaFactory::saveHeader(str);
0090 
0091     str << qint32(m_nameDictOffset);
0092     str << qint32(m_relNameDictOffset);
0093     str << qint32(m_offerListOffset);
0094     str << qint32(m_menuIdDictOffset);
0095 }
0096 
0097 void KBuildServiceFactory::save(QDataStream &str)
0098 {
0099     KSycocaFactory::save(str);
0100 
0101     m_nameDictOffset = str.device()->pos();
0102     m_nameDict->save(str);
0103 
0104     m_relNameDictOffset = str.device()->pos();
0105     m_relNameDict->save(str);
0106 
0107     saveOfferList(str);
0108 
0109     m_menuIdDictOffset = str.device()->pos();
0110     m_menuIdDict->save(str);
0111 
0112     qint64 endOfFactoryData = str.device()->pos();
0113 
0114     // Update header (pass #3)
0115     saveHeader(str);
0116 
0117     // Seek to end.
0118     str.device()->seek(endOfFactoryData);
0119 }
0120 
0121 void KBuildServiceFactory::collectInheritedServices()
0122 {
0123     // For each MIME type, go up the parent MIME type chains and collect offers.
0124     // For "removed associations" to work, we can't just grab everything from all parents.
0125     // We need to process parents before children, hence the recursive call in
0126     // collectInheritedServices(mime) and the QSet to process a given parent only once.
0127     QSet<QString> visitedMimes;
0128     const auto lst = m_mimeTypeFactory->allMimeTypes();
0129     for (const QString &mimeType : lst) {
0130         collectInheritedServices(mimeType, visitedMimes);
0131     }
0132 }
0133 
0134 void KBuildServiceFactory::collectInheritedServices(const QString &mimeTypeName, QSet<QString> &visitedMimes)
0135 {
0136     if (visitedMimes.contains(mimeTypeName)) {
0137         return;
0138     }
0139     visitedMimes.insert(mimeTypeName);
0140 
0141     QMimeDatabase db;
0142     const QMimeType qmime = db.mimeTypeForName(mimeTypeName);
0143     const auto lst = qmime.parentMimeTypes();
0144     for (QString parentMimeType : lst) {
0145         // Workaround issue in shared-mime-info and/or Qt, which sometimes return an alias as parent
0146         parentMimeType = db.mimeTypeForName(parentMimeType).name();
0147 
0148         collectInheritedServices(parentMimeType, visitedMimes);
0149 
0150         const QList<KServiceOffer> &offers = m_offerHash.offersFor(parentMimeType);
0151         for (const auto &serviceOffer : offers) {
0152             if (!m_offerHash.hasRemovedOffer(mimeTypeName, serviceOffer.service())) {
0153                 KServiceOffer offer(serviceOffer);
0154                 offer.setMimeTypeInheritanceLevel(offer.mimeTypeInheritanceLevel() + 1);
0155                 // qCDebug(SYCOCA) << "INHERITANCE: Adding service" << (*itserv).service()->entryPath() << "to" << mimeTypeName << "mimeTypeInheritanceLevel="
0156                 // << mimeTypeInheritanceLevel;
0157                 m_offerHash.addServiceOffer(mimeTypeName, offer);
0158             }
0159         }
0160     }
0161 }
0162 
0163 void KBuildServiceFactory::postProcessServices()
0164 {
0165     // By doing all this here rather than in addEntry (and removing when replacing
0166     // with local override), we only do it for the final applications.
0167     // Note that this also affects resolution of the by-desktop-name lookup,
0168     // as name resolution is only performed *after* all the duplicates (based on
0169     // storage ID) have been removed.
0170 
0171     // For every service...
0172     for (auto itserv = m_entryDict->cbegin(), endIt = m_entryDict->cend(); itserv != endIt; ++itserv) {
0173         KSycocaEntry::Ptr entry = itserv.value();
0174         KService::Ptr service(static_cast<KService *>(entry.data()));
0175 
0176         const QString name = service->desktopEntryName();
0177         KService::Ptr dup = m_nameMemoryHash.value(name);
0178         if (dup) {
0179             // The rule is that searching for the desktop name "foo" should find
0180             // the desktop file with the storage id "foo.desktop" before it
0181             // finds "bar/foo.desktop" (or "bar-foo.desktop").
0182             // "bar/foo.desktop" and "baz/foo.desktop" are arbitrarily ordered
0183             // (in practice, the one later in the alphabet wins).
0184             if (dup->storageId().endsWith(service->storageId())) {
0185                 // allow dup to be overridden
0186                 m_nameDict->remove(name);
0187                 dup = nullptr;
0188             }
0189         }
0190         if (!dup) {
0191             m_nameDict->add(name, entry);
0192             m_nameMemoryHash.insert(name, service);
0193         }
0194 
0195         const QString relName = service->entryPath();
0196         // qCDebug(SYCOCA) << "adding service" << service.data() << "isApp=" << service->isApplication() << "menuId=" << service->menuId() << "name=" << name <<
0197         // "relName=" << relName;
0198         m_relNameDict->add(relName, entry);
0199         m_relNameMemoryHash.insert(relName, service); // for KMimeAssociations
0200 
0201         const QString menuId = service->menuId();
0202         if (!menuId.isEmpty()) { // empty for services, non-empty for applications
0203             m_menuIdDict->add(menuId, entry);
0204             m_menuIdMemoryHash.insert(menuId, service); // for KMimeAssociations
0205         }
0206     }
0207     populateServiceTypes();
0208 }
0209 
0210 void KBuildServiceFactory::populateServiceTypes()
0211 {
0212     QMimeDatabase db;
0213     // For every service...
0214     for (auto servIt = m_entryDict->cbegin(), endIt = m_entryDict->cend(); servIt != endIt; ++servIt) {
0215         KService::Ptr service(static_cast<KService *>(servIt.value().data()));
0216         const bool hidden = !service->showInCurrentDesktop();
0217 
0218         QList<KService::ServiceTypeAndPreference> serviceTypeList = service->_k_accessServiceTypes();
0219 
0220         // Add this service to all its MIME types
0221         // Don't cache count(), it can change during iteration! (we can't use an iterator-based loop
0222         // here the container could get reallocated which would invalidate iterators)
0223         for (int i = 0; i < serviceTypeList.count(); ++i) {
0224             const KService::ServiceTypeAndPreference &typeAndPref = serviceTypeList.at(i);
0225             const QString stName = typeAndPref.serviceType;
0226 
0227             if (hidden) {
0228                 continue;
0229             }
0230             const int preference = typeAndPref.preference;
0231 
0232             KServiceOffer offer(service, preference, 0);
0233             QMimeType mime = db.mimeTypeForName(stName);
0234             if (!mime.isValid()) {
0235                 if (stName.startsWith(QLatin1String("x-scheme-handler/"))) {
0236                     // Create those on demand
0237                     m_mimeTypeFactory->createFakeMimeType(stName);
0238                     m_offerHash.addServiceOffer(stName, offer);
0239                 } else {
0240                     qCDebug(SYCOCA) << service->entryPath() << "specifies undefined MIME type/servicetype" << stName;
0241                     // technically we could call addServiceOffer here, 'mime' isn't used. But it
0242                     // would be useless, since we have no MIME type entry where to write the offers offset.
0243                     continue;
0244                 }
0245             } else {
0246                 bool shouldAdd = true;
0247                 const auto lst = service->mimeTypes();
0248 
0249                 for (const QString &otherType : lst) {
0250                     // Skip derived types if the base class is listed (#321706)
0251                     if (stName != otherType && mime.inherits(otherType)) {
0252                         // But don't skip aliases (they got resolved into mime.name() already, but don't let two aliases cancel out)
0253                         if (db.mimeTypeForName(otherType).name() != mime.name()) {
0254                             // qCDebug(SYCOCA) << "Skipping" << mime.name() << "because of" << otherType << "(canonical" << db.mimeTypeForName(otherType) <<
0255                             // ") while parsing" << service->entryPath();
0256                             shouldAdd = false;
0257                         }
0258                     }
0259                 }
0260                 if (shouldAdd) {
0261                     // qCDebug(SYCOCA) << "Adding service" << service->entryPath() << "to" << mime.name();
0262                     m_offerHash.addServiceOffer(mime.name(), offer); // mime.name() so that we resolve aliases
0263                 }
0264             }
0265         }
0266     }
0267 
0268     // Read user preferences (added/removed associations) and add/remove serviceoffers to m_offerHash
0269     KMimeAssociations mimeAssociations(m_offerHash, this);
0270     mimeAssociations.parseAllMimeAppsList();
0271 
0272     // Now for each MIME type, collect services from parent MIME types
0273     collectInheritedServices();
0274 
0275     // Now collect the offsets into the (future) offer list
0276     // The loops look very much like the ones in saveOfferList obviously.
0277     int offersOffset = 0;
0278     const int offerEntrySize = sizeof(qint32) * 4; // four qint32s, see saveOfferList.
0279 
0280     const auto &offerHash = m_offerHash.serviceTypeData();
0281     auto it = offerHash.constBegin();
0282     const auto end = offerHash.constEnd();
0283     for (; it != end; ++it) {
0284         const QString stName = it.key();
0285         const ServiceTypeOffersData offersData = it.value();
0286         const int numOffers = offersData.offers.count();
0287 
0288         KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(stName);
0289         if (entry) {
0290             entry->setServiceOffersOffset(offersOffset);
0291             offersOffset += offerEntrySize * numOffers;
0292         } else if (stName.startsWith(QLatin1String("x-scheme-handler/"))) {
0293             // Create those on demand
0294             entry = m_mimeTypeFactory->createFakeMimeType(stName);
0295             entry->setServiceOffersOffset(offersOffset);
0296             offersOffset += offerEntrySize * numOffers;
0297         } else {
0298             if (stName.isEmpty()) {
0299                 qCDebug(SYCOCA) << "Empty service type";
0300             } else {
0301                 qCWarning(SYCOCA) << "Service type not found:" << stName;
0302             }
0303         }
0304     }
0305 }
0306 
0307 void KBuildServiceFactory::saveOfferList(QDataStream &str)
0308 {
0309     m_offerListOffset = str.device()->pos();
0310     // qCDebug(SYCOCA) << "Saving offer list at offset" << m_offerListOffset;
0311 
0312     const auto &offerHash = m_offerHash.serviceTypeData();
0313     auto it = offerHash.constBegin();
0314     const auto end = offerHash.constEnd();
0315     for (; it != end; ++it) {
0316         const QString stName = it.key();
0317         const ServiceTypeOffersData offersData = it.value();
0318         QList<KServiceOffer> offers = offersData.offers;
0319         std::stable_sort(offers.begin(), offers.end()); // by initial preference
0320 
0321         int offset = -1;
0322         KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(stName);
0323         if (entry) {
0324             offset = entry->offset();
0325             // Q_ASSERT(str.device()->pos() == entry->serviceOffersOffset() + m_offerListOffset);
0326         }
0327         if (offset == -1) {
0328             qCDebug(SYCOCA) << "Didn't find servicetype or MIME type" << stName;
0329             continue;
0330         }
0331 
0332         for (const auto &offer : std::as_const(offers)) {
0333             // qCDebug(SYCOCA) << stName << ": writing offer" << offer.service()->desktopEntryName() << offset << offer.service()->offset() << "in sycoca at
0334             // pos" << str.device()->pos();
0335             Q_ASSERT(offer.service()->offset() != 0);
0336 
0337             str << qint32(offset);
0338             str << qint32(offer.service()->offset());
0339             str << qint32(offer.preference());
0340             str << qint32(offer.mimeTypeInheritanceLevel());
0341             // update offerEntrySize in populateServiceTypes if you add/remove something here
0342         }
0343     }
0344 
0345     str << qint32(0); // End of list marker (0)
0346 }
0347 
0348 void KBuildServiceFactory::addEntry(const KSycocaEntry::Ptr &newEntry)
0349 {
0350     Q_ASSERT(newEntry);
0351     if (m_dupeDict.contains(newEntry)) {
0352         return;
0353     }
0354 
0355     const KService::Ptr service(static_cast<KService *>(newEntry.data()));
0356     m_dupeDict.insert(newEntry);
0357     KSycocaFactory::addEntry(newEntry);
0358 }