File indexing completed on 2024-10-06 12:23:59

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