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 }