File indexing completed on 2024-04-28 15:29:54

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2008 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "kmimeassociations_p.h"
0009 #include "sycocadebug.h"
0010 #include <KConfig>
0011 #include <KConfigGroup>
0012 #include <QDebug>
0013 #include <QFile>
0014 #include <QMimeDatabase>
0015 #include <QStandardPaths>
0016 #include <kservice.h>
0017 #include <kservicefactory_p.h>
0018 
0019 KMimeAssociations::KMimeAssociations(KOfferHash &offerHash, KServiceFactory *serviceFactory)
0020     : m_offerHash(offerHash)
0021     , m_serviceFactory(serviceFactory)
0022 {
0023 }
0024 
0025 /*
0026 
0027 The goal of this class is to parse mimeapps.list files, which are used to
0028 let users configure the application-MIME type associations.
0029 
0030 Example file:
0031 
0032 [Added Associations]
0033 text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
0034 
0035 [Removed Associations]
0036 text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
0037 
0038 [Default Applications]
0039 text/plain=kate.desktop;
0040 */
0041 
0042 QStringList KMimeAssociations::mimeAppsFiles()
0043 {
0044     QStringList mimeappsFileNames;
0045     // make the list of possible filenames from the spec ($desktop-mimeapps.list, then mimeapps.list)
0046     const QString desktops = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP"));
0047     const auto list = desktops.split(QLatin1Char(':'), Qt::SkipEmptyParts);
0048     for (const QString &desktop : list) {
0049         mimeappsFileNames.append(desktop.toLower() + QLatin1String("-mimeapps.list"));
0050     }
0051     mimeappsFileNames.append(QStringLiteral("mimeapps.list"));
0052     const QStringList mimeappsDirs = mimeAppsDirs();
0053     QStringList mimeappsFiles;
0054     // collect existing files
0055     for (const QString &dir : mimeappsDirs) {
0056         for (const QString &file : std::as_const(mimeappsFileNames)) {
0057             const QString filePath = dir + QLatin1Char('/') + file;
0058             if (QFile::exists(filePath) && !mimeappsFiles.contains(filePath)) {
0059                 mimeappsFiles.append(filePath);
0060             }
0061         }
0062     }
0063     return mimeappsFiles;
0064 }
0065 
0066 QStringList KMimeAssociations::mimeAppsDirs()
0067 {
0068     // list the dirs in the order of the spec (XDG_CONFIG_HOME, XDG_CONFIG_DIRS, XDG_DATA_HOME, XDG_DATA_DIRS)
0069     return QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation) + QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
0070 }
0071 
0072 void KMimeAssociations::parseAllMimeAppsList()
0073 {
0074     int basePreference = 1000; // start high :)
0075     const QStringList files = KMimeAssociations::mimeAppsFiles();
0076     // Global first, then local
0077     auto it = files.crbegin();
0078     auto endIt = files.crend();
0079     for (; it != endIt; ++it) {
0080         // qDebug() << "Parsing" << mimeappsFile;
0081         parseMimeAppsList(*it, basePreference);
0082         basePreference += 50;
0083     }
0084 }
0085 
0086 void KMimeAssociations::parseMimeAppsList(const QString &file, int basePreference)
0087 {
0088     KConfig profile(file, KConfig::SimpleConfig);
0089     if (file.endsWith(QLatin1String("/mimeapps.list"))) { // not for $desktop-mimeapps.list
0090         parseAddedAssociations(KConfigGroup(&profile, "Added Associations"), file, basePreference);
0091         parseRemovedAssociations(KConfigGroup(&profile, "Removed Associations"), file);
0092 
0093         // KDE extension for parts and plugins, see settings/filetypes/mimetypedata.cpp
0094         parseAddedAssociations(KConfigGroup(&profile, "Added KDE Service Associations"), file, basePreference);
0095         parseRemovedAssociations(KConfigGroup(&profile, "Removed KDE Service Associations"), file);
0096     }
0097 
0098     // Default Applications is preferred over Added Associations.
0099     // Other than that, they work the same...
0100     // add 25 to the basePreference to make sure those service offers will have higher preferences
0101     // 25 is arbitrary half of the allocated preference indices for the current parsed mimeapps.list file, defined line 86
0102     parseAddedAssociations(KConfigGroup(&profile, "Default Applications"), file, basePreference + 25);
0103 }
0104 
0105 void KMimeAssociations::parseAddedAssociations(const KConfigGroup &group, const QString &file, int basePreference)
0106 {
0107     Q_UNUSED(file) // except in debug statements
0108     QMimeDatabase db;
0109     const auto keyList = group.keyList();
0110     for (const QString &mimeName : keyList) {
0111         const QStringList services = group.readXdgListEntry(mimeName);
0112         const QString resolvedMimeName = mimeName.startsWith(QLatin1String("x-scheme-handler/")) ? mimeName : db.mimeTypeForName(mimeName).name();
0113         if (resolvedMimeName.isEmpty()) {
0114             qCDebug(SYCOCA) << file << "specifies unknown MIME type" << mimeName << "in" << group.name();
0115         } else {
0116             int pref = basePreference;
0117             for (const QString &service : services) {
0118                 KService::Ptr pService = m_serviceFactory->findServiceByStorageId(service);
0119                 if (!pService) {
0120                     qCDebug(SYCOCA) << file << "specifies unknown service" << service << "in" << group.name();
0121                 } else {
0122                     // qDebug() << "adding mime" << resolvedMimeName << "to service" << pService->entryPath() << "pref=" << pref;
0123 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 69)
0124                     m_offerHash.addServiceOffer(resolvedMimeName, KServiceOffer(pService, pref, 0, pService->allowAsDefault()));
0125 #else
0126                     m_offerHash.addServiceOffer(resolvedMimeName, KServiceOffer(pService, pref, 0));
0127 #endif
0128                     --pref;
0129                 }
0130             }
0131         }
0132     }
0133 }
0134 
0135 void KMimeAssociations::parseRemovedAssociations(const KConfigGroup &group, const QString &file)
0136 {
0137     Q_UNUSED(file) // except in debug statements
0138     const auto keyList = group.keyList();
0139     for (const QString &mime : keyList) {
0140         const QStringList services = group.readXdgListEntry(mime);
0141         for (const QString &service : services) {
0142             KService::Ptr pService = m_serviceFactory->findServiceByStorageId(service);
0143             if (!pService) {
0144                 // qDebug() << file << "specifies unknown service" << service << "in" << group.name();
0145             } else {
0146                 // qDebug() << "removing mime" << mime << "from service" << pService.data() << pService->entryPath();
0147                 m_offerHash.removeServiceOffer(mime, pService);
0148             }
0149         }
0150     }
0151 }
0152 
0153 void KOfferHash::addServiceOffer(const QString &serviceType, const KServiceOffer &offer)
0154 {
0155     KService::Ptr service = offer.service();
0156     // qDebug() << "Adding" << service->entryPath() << "to" << serviceType << offer.preference();
0157     ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create
0158     QList<KServiceOffer> &offers = data.offers;
0159     QSet<KService::Ptr> &offerSet = data.offerSet;
0160     if (!offerSet.contains(service)) {
0161         offers.append(offer);
0162         offerSet.insert(service);
0163     } else {
0164         const int initPref = offer.preference();
0165         // qDebug() << service->entryPath() << "already in" << serviceType;
0166         // This happens when mimeapps.list mentions a service (to make it preferred)
0167         // Update initialPreference to std::max(existing offer, new offer)
0168         for (KServiceOffer &servOffer : data.offers) {
0169             if (servOffer.service() == service) { // we can compare KService::Ptrs because they are from the memory hash
0170                 servOffer.setPreference(std::max(servOffer.preference(), initPref));
0171             }
0172         }
0173     }
0174 }
0175 
0176 void KOfferHash::removeServiceOffer(const QString &serviceType, const KService::Ptr &service)
0177 {
0178     ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create
0179     data.removedOffers.insert(service);
0180     data.offerSet.remove(service);
0181 
0182     const QString id = service->storageId();
0183 
0184     auto &list = data.offers;
0185     auto it = std::remove_if(list.begin(), list.end(), [&id](const KServiceOffer &offer) {
0186         return offer.service()->storageId() == id;
0187     });
0188     list.erase(it, list.end());
0189 }
0190 
0191 bool KOfferHash::hasRemovedOffer(const QString &serviceType, const KService::Ptr &service) const
0192 {
0193     auto it = m_serviceTypeData.constFind(serviceType);
0194     if (it != m_serviceTypeData.cend()) {
0195         return it.value().removedOffers.contains(service);
0196     }
0197     return false;
0198 }