1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 1999, 2007 David Faure <faure@kde.org> |
4 | SPDX-FileCopyrightText: 1999 Waldo Bastian <bastian@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-only |
7 | */ |
8 | |
9 | #include "kbuildmimetypefactory_p.h" |
10 | #include "kbuildservicefactory_p.h" |
11 | #include "kbuildservicegroupfactory_p.h" |
12 | #include "ksycoca.h" |
13 | |
14 | #include "ksycocadict_p.h" |
15 | #include "sycocadebug.h" |
16 | #include <KDesktopFile> |
17 | |
18 | #include <QDebug> |
19 | #include <QDir> |
20 | #include <QMimeDatabase> |
21 | |
22 | #include <QStandardPaths> |
23 | #include <kmimetypefactory_p.h> |
24 | #include <kservice_p.h> |
25 | |
26 | KBuildServiceFactory::KBuildServiceFactory(KBuildMimeTypeFactory *mimeTypeFactory) |
27 | : KServiceFactory(mimeTypeFactory->sycoca()) |
28 | , m_nameMemoryHash() |
29 | , m_relNameMemoryHash() |
30 | , m_menuIdMemoryHash() |
31 | , m_dupeDict() |
32 | , m_mimeTypeFactory(mimeTypeFactory) |
33 | { |
34 | m_nameDict = new KSycocaDict(); |
35 | m_relNameDict = new KSycocaDict(); |
36 | m_menuIdDict = new KSycocaDict(); |
37 | } |
38 | |
39 | KBuildServiceFactory::~KBuildServiceFactory() |
40 | { |
41 | } |
42 | |
43 | KService::Ptr KBuildServiceFactory::findServiceByDesktopName(const QString &name) |
44 | { |
45 | return m_nameMemoryHash.value(key: name); |
46 | } |
47 | |
48 | KService::Ptr KBuildServiceFactory::findServiceByDesktopPath(const QString &name) |
49 | { |
50 | return m_relNameMemoryHash.value(key: name); |
51 | } |
52 | |
53 | KService::Ptr KBuildServiceFactory::(const QString &) |
54 | { |
55 | return m_menuIdMemoryHash.value(key: menuId); |
56 | } |
57 | |
58 | KSycocaEntry *KBuildServiceFactory::createEntry(const QString &file) const |
59 | { |
60 | const QStringView name = QStringView(file).mid(pos: file.lastIndexOf(c: QLatin1Char('/')) + 1); |
61 | |
62 | // Is it a .desktop file? |
63 | if (name.endsWith(s: QLatin1String(".desktop" ))) { |
64 | // qCDebug(SYCOCA) << file; |
65 | |
66 | Q_ASSERT(QDir::isAbsolutePath(file)); |
67 | KService *serv = new KService(file); |
68 | |
69 | // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath(); |
70 | // Note that the menuId will be set by the vfolder_menu.cpp code just after |
71 | // createEntry returns. |
72 | |
73 | if (serv->isValid() && !serv->isDeleted()) { |
74 | // qCDebug(SYCOCA) << "Creating KService from" << file << "entryPath=" << serv->entryPath() << "storageId=" << serv->storageId(); |
75 | return serv; |
76 | } else { |
77 | if (!serv->isDeleted()) { |
78 | qCWarning(SYCOCA) << "Invalid Service : " << file; |
79 | } |
80 | delete serv; |
81 | return nullptr; |
82 | } |
83 | } // TODO else if a Windows application, new KService(name, exec, icon) |
84 | return nullptr; |
85 | } |
86 | |
87 | void KBuildServiceFactory::(QDataStream &str) |
88 | { |
89 | KSycocaFactory::saveHeader(str); |
90 | |
91 | str << qint32(m_nameDictOffset); |
92 | str << qint32(m_relNameDictOffset); |
93 | str << qint32(m_offerListOffset); |
94 | str << qint32(m_menuIdDictOffset); |
95 | } |
96 | |
97 | void KBuildServiceFactory::save(QDataStream &str) |
98 | { |
99 | KSycocaFactory::save(str); |
100 | |
101 | m_nameDictOffset = str.device()->pos(); |
102 | m_nameDict->save(str); |
103 | |
104 | m_relNameDictOffset = str.device()->pos(); |
105 | m_relNameDict->save(str); |
106 | |
107 | saveOfferList(str); |
108 | |
109 | m_menuIdDictOffset = str.device()->pos(); |
110 | m_menuIdDict->save(str); |
111 | |
112 | qint64 endOfFactoryData = str.device()->pos(); |
113 | |
114 | // Update header (pass #3) |
115 | saveHeader(str); |
116 | |
117 | // Seek to end. |
118 | str.device()->seek(pos: endOfFactoryData); |
119 | } |
120 | |
121 | void KBuildServiceFactory::collectInheritedServices() |
122 | { |
123 | // For each MIME type, go up the parent MIME type chains and collect offers. |
124 | // For "removed associations" to work, we can't just grab everything from all parents. |
125 | // We need to process parents before children, hence the recursive call in |
126 | // collectInheritedServices(mime) and the QSet to process a given parent only once. |
127 | QSet<QString> visitedMimes; |
128 | const auto lst = m_mimeTypeFactory->allMimeTypes(); |
129 | for (const QString &mimeType : lst) { |
130 | collectInheritedServices(mime: mimeType, visitedMimes); |
131 | } |
132 | } |
133 | |
134 | void KBuildServiceFactory::collectInheritedServices(const QString &mimeTypeName, QSet<QString> &visitedMimes) |
135 | { |
136 | if (visitedMimes.contains(value: mimeTypeName)) { |
137 | return; |
138 | } |
139 | visitedMimes.insert(value: mimeTypeName); |
140 | |
141 | QMimeDatabase db; |
142 | const QMimeType qmime = db.mimeTypeForName(nameOrAlias: mimeTypeName); |
143 | const auto lst = qmime.parentMimeTypes(); |
144 | for (QString parentMimeType : lst) { |
145 | // Workaround issue in shared-mime-info and/or Qt, which sometimes return an alias as parent |
146 | parentMimeType = db.mimeTypeForName(nameOrAlias: parentMimeType).name(); |
147 | |
148 | collectInheritedServices(mimeTypeName: parentMimeType, visitedMimes); |
149 | |
150 | const QList<KServiceOffer> &offers = m_offerHash.offersFor(serviceType: parentMimeType); |
151 | for (const auto &serviceOffer : offers) { |
152 | if (!m_offerHash.hasRemovedOffer(serviceType: mimeTypeName, service: serviceOffer.service())) { |
153 | KServiceOffer offer(serviceOffer); |
154 | offer.setMimeTypeInheritanceLevel(offer.mimeTypeInheritanceLevel() + 1); |
155 | // qCDebug(SYCOCA) << "INHERITANCE: Adding service" << (*itserv).service()->entryPath() << "to" << mimeTypeName << "mimeTypeInheritanceLevel=" |
156 | // << mimeTypeInheritanceLevel; |
157 | m_offerHash.addServiceOffer(serviceType: mimeTypeName, offer); |
158 | } |
159 | } |
160 | } |
161 | } |
162 | |
163 | void KBuildServiceFactory::postProcessServices() |
164 | { |
165 | // By doing all this here rather than in addEntry (and removing when replacing |
166 | // with local override), we only do it for the final applications. |
167 | // Note that this also affects resolution of the by-desktop-name lookup, |
168 | // as name resolution is only performed *after* all the duplicates (based on |
169 | // storage ID) have been removed. |
170 | |
171 | // For every service... |
172 | for (auto itserv = m_entryDict->cbegin(), endIt = m_entryDict->cend(); itserv != endIt; ++itserv) { |
173 | KSycocaEntry::Ptr entry = itserv.value(); |
174 | KService::Ptr service(static_cast<KService *>(entry.data())); |
175 | |
176 | const QString name = service->desktopEntryName(); |
177 | KService::Ptr dup = m_nameMemoryHash.value(key: name); |
178 | if (dup) { |
179 | // The rule is that searching for the desktop name "foo" should find |
180 | // the desktop file with the storage id "foo.desktop" before it |
181 | // finds "bar/foo.desktop" (or "bar-foo.desktop"). |
182 | // "bar/foo.desktop" and "baz/foo.desktop" are arbitrarily ordered |
183 | // (in practice, the one later in the alphabet wins). |
184 | if (dup->storageId().endsWith(s: service->storageId())) { |
185 | // allow dup to be overridden |
186 | m_nameDict->remove(key: name); |
187 | dup = nullptr; |
188 | } |
189 | } |
190 | if (!dup) { |
191 | m_nameDict->add(key: name, payload: entry); |
192 | m_nameMemoryHash.insert(key: name, value: service); |
193 | } |
194 | |
195 | const QString relName = service->entryPath(); |
196 | // qCDebug(SYCOCA) << "adding service" << service.data() << "isApp=" << service->isApplication() << "menuId=" << service->menuId() << "name=" << name << |
197 | // "relName=" << relName; |
198 | m_relNameDict->add(key: relName, payload: entry); |
199 | m_relNameMemoryHash.insert(key: relName, value: service); // for KMimeAssociations |
200 | |
201 | const QString = service->menuId(); |
202 | if (!menuId.isEmpty()) { // empty for services, non-empty for applications |
203 | m_menuIdDict->add(key: menuId, payload: entry); |
204 | m_menuIdMemoryHash.insert(key: menuId, value: service); // for KMimeAssociations |
205 | } |
206 | } |
207 | populateServiceTypes(); |
208 | } |
209 | |
210 | void KBuildServiceFactory::populateServiceTypes() |
211 | { |
212 | QMimeDatabase db; |
213 | // For every service... |
214 | for (auto servIt = m_entryDict->cbegin(), endIt = m_entryDict->cend(); servIt != endIt; ++servIt) { |
215 | KService::Ptr service(static_cast<KService *>(servIt.value().data())); |
216 | const bool hidden = !service->showInCurrentDesktop(); |
217 | |
218 | const auto mimeTypes = service->d_func()->m_mimeTypes; |
219 | |
220 | // Add this service to all its MIME types |
221 | // Don't cache count(), it can change during iteration! (we can't use an iterator-based loop |
222 | // here the container could get reallocated which would invalidate iterators) |
223 | for (int i = 0; i < mimeTypes.count(); ++i) { |
224 | const QString &mimeName = mimeTypes.at(i); |
225 | |
226 | if (hidden) { |
227 | continue; |
228 | } |
229 | |
230 | KServiceOffer offer(service, 1 /* preference; always 1 here, may be higher based on KMimeAssociations */, 0); |
231 | QMimeType mime = db.mimeTypeForName(nameOrAlias: mimeName); |
232 | if (!mime.isValid()) { |
233 | if (mimeName.startsWith(s: QLatin1String("x-scheme-handler/" ))) { |
234 | // Create those on demand |
235 | m_mimeTypeFactory->createFakeMimeType(name: mimeName); |
236 | m_offerHash.addServiceOffer(serviceType: mimeName, offer); |
237 | } else { |
238 | qCDebug(SYCOCA) << service->entryPath() << "specifies undefined MIME type/servicetype" << mimeName; |
239 | // technically we could call addServiceOffer here, 'mime' isn't used. But it |
240 | // would be useless, since we have no MIME type entry where to write the offers offset. |
241 | continue; |
242 | } |
243 | } else { |
244 | bool shouldAdd = true; |
245 | const auto lst = service->mimeTypes(); |
246 | |
247 | for (const QString &otherType : lst) { |
248 | // Skip derived types if the base class is listed (#321706) |
249 | if (mimeName != otherType && mime.inherits(mimeTypeName: otherType)) { |
250 | // But don't skip aliases (they got resolved into mime.name() already, but don't let two aliases cancel out) |
251 | if (db.mimeTypeForName(nameOrAlias: otherType).name() != mime.name()) { |
252 | // qCDebug(SYCOCA) << "Skipping" << mime.name() << "because of" << otherType << "(canonical" << db.mimeTypeForName(otherType) << |
253 | // ") while parsing" << service->entryPath(); |
254 | shouldAdd = false; |
255 | } |
256 | } |
257 | } |
258 | if (shouldAdd) { |
259 | // qCDebug(SYCOCA) << "Adding service" << service->entryPath() << "to" << mime.name(); |
260 | m_offerHash.addServiceOffer(serviceType: mime.name(), offer); // mime.name() so that we resolve aliases |
261 | } |
262 | } |
263 | } |
264 | } |
265 | |
266 | // Read user preferences (added/removed associations) and add/remove serviceoffers to m_offerHash |
267 | KMimeAssociations mimeAssociations(m_offerHash, this); |
268 | mimeAssociations.parseAllMimeAppsList(); |
269 | |
270 | // Now for each MIME type, collect services from parent MIME types |
271 | collectInheritedServices(); |
272 | |
273 | // Now collect the offsets into the (future) offer list |
274 | // The loops look very much like the ones in saveOfferList obviously. |
275 | int offersOffset = 0; |
276 | const int offerEntrySize = sizeof(qint32) * 4; // four qint32s, see saveOfferList. |
277 | |
278 | const auto &offerHash = m_offerHash.serviceTypeData(); |
279 | auto it = offerHash.constBegin(); |
280 | const auto end = offerHash.constEnd(); |
281 | for (; it != end; ++it) { |
282 | const QString stName = it.key(); |
283 | const ServiceTypeOffersData offersData = it.value(); |
284 | const int numOffers = offersData.offers.count(); |
285 | |
286 | KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(name: stName); |
287 | if (entry) { |
288 | entry->setServiceOffersOffset(offersOffset); |
289 | offersOffset += offerEntrySize * numOffers; |
290 | } else if (stName.startsWith(s: QLatin1String("x-scheme-handler/" ))) { |
291 | // Create those on demand |
292 | entry = m_mimeTypeFactory->createFakeMimeType(name: stName); |
293 | entry->setServiceOffersOffset(offersOffset); |
294 | offersOffset += offerEntrySize * numOffers; |
295 | } else { |
296 | if (stName.isEmpty()) { |
297 | qCDebug(SYCOCA) << "Empty service type" ; |
298 | } else { |
299 | qCWarning(SYCOCA) << "Service type not found:" << stName; |
300 | } |
301 | } |
302 | } |
303 | } |
304 | |
305 | void KBuildServiceFactory::saveOfferList(QDataStream &str) |
306 | { |
307 | m_offerListOffset = str.device()->pos(); |
308 | // qCDebug(SYCOCA) << "Saving offer list at offset" << m_offerListOffset; |
309 | |
310 | const auto &offerHash = m_offerHash.serviceTypeData(); |
311 | auto it = offerHash.constBegin(); |
312 | const auto end = offerHash.constEnd(); |
313 | for (; it != end; ++it) { |
314 | const QString stName = it.key(); |
315 | const ServiceTypeOffersData offersData = it.value(); |
316 | QList<KServiceOffer> offers = offersData.offers; |
317 | std::stable_sort(first: offers.begin(), last: offers.end()); // by initial preference |
318 | |
319 | int offset = -1; |
320 | KMimeTypeFactory::MimeTypeEntry::Ptr entry = m_mimeTypeFactory->findMimeTypeEntryByName(name: stName); |
321 | if (entry) { |
322 | offset = entry->offset(); |
323 | // Q_ASSERT(str.device()->pos() == entry->serviceOffersOffset() + m_offerListOffset); |
324 | } |
325 | if (offset == -1) { |
326 | qCDebug(SYCOCA) << "Didn't find servicetype or MIME type" << stName; |
327 | continue; |
328 | } |
329 | |
330 | for (const auto &offer : std::as_const(t&: offers)) { |
331 | // qCDebug(SYCOCA) << stName << ": writing offer" << offer.service()->desktopEntryName() << offset << offer.service()->offset() << "in sycoca at |
332 | // pos" << str.device()->pos(); |
333 | Q_ASSERT(offer.service()->offset() != 0); |
334 | |
335 | str << qint32(offset); |
336 | str << qint32(offer.service()->offset()); |
337 | str << qint32(offer.preference()); |
338 | str << qint32(offer.mimeTypeInheritanceLevel()); |
339 | // update offerEntrySize in populateServiceTypes if you add/remove something here |
340 | } |
341 | } |
342 | |
343 | str << qint32(0); // End of list marker (0) |
344 | } |
345 | |
346 | void KBuildServiceFactory::addEntry(const KSycocaEntry::Ptr &newEntry) |
347 | { |
348 | Q_ASSERT(newEntry); |
349 | if (m_dupeDict.contains(value: newEntry)) { |
350 | return; |
351 | } |
352 | |
353 | const KService::Ptr service(static_cast<KService *>(newEntry.data())); |
354 | m_dupeDict.insert(value: newEntry); |
355 | KSycocaFactory::addEntry(newEntry); |
356 | } |
357 | |