1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2008 David Faure <faure@kde.org> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
6 | */ |
7 | |
8 | #include "kmimeassociations_p.h" |
9 | #include "sycocadebug.h" |
10 | #include <KConfig> |
11 | #include <KConfigGroup> |
12 | #include <QDebug> |
13 | #include <QFile> |
14 | #include <QMimeDatabase> |
15 | #include <QStandardPaths> |
16 | #include <kservice.h> |
17 | #include <kservicefactory_p.h> |
18 | |
19 | KMimeAssociations::KMimeAssociations(KOfferHash &offerHash, KServiceFactory *serviceFactory) |
20 | : m_offerHash(offerHash) |
21 | , m_serviceFactory(serviceFactory) |
22 | { |
23 | } |
24 | |
25 | /* |
26 | |
27 | The goal of this class is to parse mimeapps.list files, which are used to |
28 | let users configure the application-MIME type associations. |
29 | |
30 | Example file: |
31 | |
32 | [Added Associations] |
33 | text/plain=gnome-gedit.desktop;gnu-emacs.desktop; |
34 | |
35 | [Removed Associations] |
36 | text/plain=gnome-gedit.desktop;gnu-emacs.desktop; |
37 | |
38 | [Default Applications] |
39 | text/plain=kate.desktop; |
40 | */ |
41 | |
42 | QStringList KMimeAssociations::mimeAppsFiles() |
43 | { |
44 | QStringList mimeappsFileNames; |
45 | // make the list of possible filenames from the spec ($desktop-mimeapps.list, then mimeapps.list) |
46 | const QString desktops = QString::fromLocal8Bit(ba: qgetenv(varName: "XDG_CURRENT_DESKTOP" )); |
47 | const auto list = desktops.split(sep: QLatin1Char(':'), behavior: Qt::SkipEmptyParts); |
48 | for (const QString &desktop : list) { |
49 | mimeappsFileNames.append(t: desktop.toLower() + QLatin1String("-mimeapps.list" )); |
50 | } |
51 | mimeappsFileNames.append(QStringLiteral("mimeapps.list" )); |
52 | const QStringList mimeappsDirs = mimeAppsDirs(); |
53 | QStringList mimeappsFiles; |
54 | // collect existing files |
55 | for (const QString &dir : mimeappsDirs) { |
56 | for (const QString &file : std::as_const(t&: mimeappsFileNames)) { |
57 | const QString filePath = dir + QLatin1Char('/') + file; |
58 | if (QFile::exists(fileName: filePath) && !mimeappsFiles.contains(str: filePath)) { |
59 | mimeappsFiles.append(t: filePath); |
60 | } |
61 | } |
62 | } |
63 | return mimeappsFiles; |
64 | } |
65 | |
66 | QStringList KMimeAssociations::mimeAppsDirs() |
67 | { |
68 | // list the dirs in the order of the spec (XDG_CONFIG_HOME, XDG_CONFIG_DIRS, XDG_DATA_HOME, XDG_DATA_DIRS) |
69 | return QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation) + QStandardPaths::standardLocations(type: QStandardPaths::ApplicationsLocation); |
70 | } |
71 | |
72 | void KMimeAssociations::parseAllMimeAppsList() |
73 | { |
74 | int basePreference = 1000; // start high :) |
75 | const QStringList files = KMimeAssociations::mimeAppsFiles(); |
76 | // Global first, then local |
77 | auto it = files.crbegin(); |
78 | auto endIt = files.crend(); |
79 | for (; it != endIt; ++it) { |
80 | // qDebug() << "Parsing" << mimeappsFile; |
81 | parseMimeAppsList(file: *it, basePreference); |
82 | basePreference += 50; |
83 | } |
84 | } |
85 | |
86 | void KMimeAssociations::parseMimeAppsList(const QString &file, int basePreference) |
87 | { |
88 | KConfig profile(file, KConfig::SimpleConfig); |
89 | if (file.endsWith(s: QLatin1String("/mimeapps.list" ))) { // not for $desktop-mimeapps.list |
90 | parseAddedAssociations(group: KConfigGroup(&profile, QStringLiteral("Added Associations" )), file, basePreference); |
91 | parseRemovedAssociations(group: KConfigGroup(&profile, QStringLiteral("Removed Associations" )), file); |
92 | |
93 | // KDE extension for parts and plugins, see settings/filetypes/mimetypedata.cpp |
94 | parseAddedAssociations(group: KConfigGroup(&profile, QStringLiteral("Added KDE Service Associations" )), file, basePreference); |
95 | parseRemovedAssociations(group: KConfigGroup(&profile, QStringLiteral("Removed KDE Service Associations" )), file); |
96 | } |
97 | |
98 | // Default Applications is preferred over Added Associations. |
99 | // Other than that, they work the same... |
100 | // add 25 to the basePreference to make sure those service offers will have higher preferences |
101 | // 25 is arbitrary half of the allocated preference indices for the current parsed mimeapps.list file, defined line 86 |
102 | parseAddedAssociations(group: KConfigGroup(&profile, QStringLiteral("Default Applications" )), file, basePreference: basePreference + 25); |
103 | } |
104 | |
105 | void KMimeAssociations::parseAddedAssociations(const KConfigGroup &group, const QString &file, int basePreference) |
106 | { |
107 | Q_UNUSED(file) // except in debug statements |
108 | QMimeDatabase db; |
109 | const auto keyList = group.keyList(); |
110 | for (const QString &mimeName : keyList) { |
111 | const QStringList services = group.readXdgListEntry(pKey: mimeName); |
112 | const QString resolvedMimeName = mimeName.startsWith(s: QLatin1String("x-scheme-handler/" )) ? mimeName : db.mimeTypeForName(nameOrAlias: mimeName).name(); |
113 | if (resolvedMimeName.isEmpty()) { |
114 | qCDebug(SYCOCA) << file << "specifies unknown MIME type" << mimeName << "in" << group.name(); |
115 | } else { |
116 | int pref = basePreference; |
117 | for (const QString &service : services) { |
118 | KService::Ptr pService = m_serviceFactory->findServiceByStorageId(storageId: service); |
119 | if (!pService) { |
120 | qCDebug(SYCOCA) << file << "specifies unknown service" << service << "in" << group.name(); |
121 | } else { |
122 | // qDebug() << "adding mime" << resolvedMimeName << "to service" << pService->entryPath() << "pref=" << pref; |
123 | m_offerHash.addServiceOffer(serviceType: resolvedMimeName, offer: KServiceOffer(pService, pref, 0)); |
124 | --pref; |
125 | } |
126 | } |
127 | } |
128 | } |
129 | } |
130 | |
131 | void KMimeAssociations::parseRemovedAssociations(const KConfigGroup &group, const QString &file) |
132 | { |
133 | Q_UNUSED(file) // except in debug statements |
134 | const auto keyList = group.keyList(); |
135 | for (const QString &mime : keyList) { |
136 | const QStringList services = group.readXdgListEntry(pKey: mime); |
137 | for (const QString &service : services) { |
138 | KService::Ptr pService = m_serviceFactory->findServiceByStorageId(storageId: service); |
139 | if (!pService) { |
140 | // qDebug() << file << "specifies unknown service" << service << "in" << group.name(); |
141 | } else { |
142 | // qDebug() << "removing mime" << mime << "from service" << pService.data() << pService->entryPath(); |
143 | m_offerHash.removeServiceOffer(serviceType: mime, service: pService); |
144 | } |
145 | } |
146 | } |
147 | } |
148 | |
149 | void KOfferHash::addServiceOffer(const QString &serviceType, const KServiceOffer &offer) |
150 | { |
151 | KService::Ptr service = offer.service(); |
152 | // qDebug() << "Adding" << service->entryPath() << "to" << serviceType << offer.preference(); |
153 | ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create |
154 | QList<KServiceOffer> &offers = data.offers; |
155 | QSet<KService::Ptr> &offerSet = data.offerSet; |
156 | if (!offerSet.contains(value: service)) { |
157 | offers.append(t: offer); |
158 | offerSet.insert(value: service); |
159 | } else { |
160 | const int initPref = offer.preference(); |
161 | // qDebug() << service->entryPath() << "already in" << serviceType; |
162 | // This happens when mimeapps.list mentions a service (to make it preferred) |
163 | // Update initialPreference to std::max(existing offer, new offer) |
164 | for (KServiceOffer &servOffer : data.offers) { |
165 | if (servOffer.service() == service) { // we can compare KService::Ptrs because they are from the memory hash |
166 | servOffer.setPreference(std::max(a: servOffer.preference(), b: initPref)); |
167 | } |
168 | } |
169 | } |
170 | } |
171 | |
172 | void KOfferHash::removeServiceOffer(const QString &serviceType, const KService::Ptr &service) |
173 | { |
174 | ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create |
175 | data.removedOffers.insert(value: service); |
176 | data.offerSet.remove(value: service); |
177 | |
178 | const QString id = service->storageId(); |
179 | |
180 | auto &list = data.offers; |
181 | auto it = std::remove_if(first: list.begin(), last: list.end(), pred: [&id](const KServiceOffer &offer) { |
182 | return offer.service()->storageId() == id; |
183 | }); |
184 | list.erase(abegin: it, aend: list.end()); |
185 | } |
186 | |
187 | bool KOfferHash::hasRemovedOffer(const QString &serviceType, const KService::Ptr &service) const |
188 | { |
189 | auto it = m_serviceTypeData.constFind(key: serviceType); |
190 | if (it != m_serviceTypeData.cend()) { |
191 | return it.value().removedOffers.contains(value: service); |
192 | } |
193 | return false; |
194 | } |
195 | |