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
19KMimeAssociations::KMimeAssociations(KOfferHash &offerHash, KServiceFactory *serviceFactory)
20 : m_offerHash(offerHash)
21 , m_serviceFactory(serviceFactory)
22{
23}
24
25/*
26
27The goal of this class is to parse mimeapps.list files, which are used to
28let users configure the application-MIME type associations.
29
30Example file:
31
32[Added Associations]
33text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
34
35[Removed Associations]
36text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
37
38[Default Applications]
39text/plain=kate.desktop;
40*/
41
42QStringList 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
66QStringList 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
72void 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
86void 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
105void 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
131void 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
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