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