| 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 | |