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
20KMimeAssociations::KMimeAssociations(KOfferHash &offerHash, KServiceFactory *serviceFactory)
21 : m_offerHash(offerHash)
22 , m_serviceFactory(serviceFactory)
23{
24}
25
26/*
27
28The goal of this class is to parse mimeapps.list files, which are used to
29let users configure the application-MIME type associations.
30
31Example file:
32
33[Added Associations]
34text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
35
36[Removed Associations]
37text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
38
39[Default Applications]
40text/plain=kate.desktop;
41*/
42
43QStringList 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
68QStringList 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
74void 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
88void 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
107void KMimeAssociations::parseAddedAssociations(const KConfigGroup &group, const QString &file, int basePreference)
108{
109 QMimeDatabase db;
110 const QStringList keyList = group.keyList();
111 for (const QString &mimeName : keyList) {
112 const QStringList services = group.readXdgListEntry(pKey: mimeName);
113 const QString resolvedMimeName = mimeName.startsWith(s: QLatin1String("x-scheme-handler/")) ? mimeName : db.mimeTypeForName(nameOrAlias: mimeName).name();
114 if (resolvedMimeName.isEmpty()) {
115 qCDebug(SYCOCA) << file << "specifies unknown MIME type" << mimeName << "in" << group.name();
116 } else {
117 int pref = basePreference;
118 for (const QString &service : services) {
119 KService::Ptr pService = m_serviceFactory->findServiceByStorageId(storageId: service);
120 if (!pService) {
121 qCDebug(SYCOCA) << file << "specifies unknown service" << service << "in" << group.name();
122 } else {
123 // qDebug() << "adding mime" << resolvedMimeName << "to service" << pService->entryPath() << "pref=" << pref;
124 m_offerHash.addServiceOffer(serviceType: resolvedMimeName, offer: KServiceOffer(pService, pref, 0));
125 --pref;
126 }
127 }
128 }
129 }
130}
131
132void KMimeAssociations::parseRemovedAssociations(const KConfigGroup &group, const QString &file)
133{
134 const QStringList 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(catFunc: SYCOCA) << 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
149void 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
172void 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
187bool 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

source code of kservice/src/sycoca/kmimeassociations.cpp