File indexing completed on 2024-07-07 03:51:52

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, QStringLiteral("Added Associations")), file, basePreference);
0091         parseRemovedAssociations(KConfigGroup(&profile, QStringLiteral("Removed Associations")), file);
0092 
0093         // KDE extension for parts and plugins, see settings/filetypes/mimetypedata.cpp
0094         parseAddedAssociations(KConfigGroup(&profile, QStringLiteral("Added KDE Service Associations")), file, basePreference);
0095         parseRemovedAssociations(KConfigGroup(&profile, QStringLiteral("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, QStringLiteral("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                     m_offerHash.addServiceOffer(resolvedMimeName, KServiceOffer(pService, pref, 0));
0124                     --pref;
0125                 }
0126             }
0127         }
0128     }
0129 }
0130 
0131 void KMimeAssociations::parseRemovedAssociations(const KConfigGroup &group, const QString &file)
0132 {
0133     Q_UNUSED(file) // except in debug statements
0134     const auto keyList = group.keyList();
0135     for (const QString &mime : keyList) {
0136         const QStringList services = group.readXdgListEntry(mime);
0137         for (const QString &service : services) {
0138             KService::Ptr pService = m_serviceFactory->findServiceByStorageId(service);
0139             if (!pService) {
0140                 // qDebug() << file << "specifies unknown service" << service << "in" << group.name();
0141             } else {
0142                 // qDebug() << "removing mime" << mime << "from service" << pService.data() << pService->entryPath();
0143                 m_offerHash.removeServiceOffer(mime, pService);
0144             }
0145         }
0146     }
0147 }
0148 
0149 void KOfferHash::addServiceOffer(const QString &serviceType, const KServiceOffer &offer)
0150 {
0151     KService::Ptr service = offer.service();
0152     // qDebug() << "Adding" << service->entryPath() << "to" << serviceType << offer.preference();
0153     ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create
0154     QList<KServiceOffer> &offers = data.offers;
0155     QSet<KService::Ptr> &offerSet = data.offerSet;
0156     if (!offerSet.contains(service)) {
0157         offers.append(offer);
0158         offerSet.insert(service);
0159     } else {
0160         const int initPref = offer.preference();
0161         // qDebug() << service->entryPath() << "already in" << serviceType;
0162         // This happens when mimeapps.list mentions a service (to make it preferred)
0163         // Update initialPreference to std::max(existing offer, new offer)
0164         for (KServiceOffer &servOffer : data.offers) {
0165             if (servOffer.service() == service) { // we can compare KService::Ptrs because they are from the memory hash
0166                 servOffer.setPreference(std::max(servOffer.preference(), initPref));
0167             }
0168         }
0169     }
0170 }
0171 
0172 void KOfferHash::removeServiceOffer(const QString &serviceType, const KService::Ptr &service)
0173 {
0174     ServiceTypeOffersData &data = m_serviceTypeData[serviceType]; // find or create
0175     data.removedOffers.insert(service);
0176     data.offerSet.remove(service);
0177 
0178     const QString id = service->storageId();
0179 
0180     auto &list = data.offers;
0181     auto it = std::remove_if(list.begin(), list.end(), [&id](const KServiceOffer &offer) {
0182         return offer.service()->storageId() == id;
0183     });
0184     list.erase(it, list.end());
0185 }
0186 
0187 bool KOfferHash::hasRemovedOffer(const QString &serviceType, const KService::Ptr &service) const
0188 {
0189     auto it = m_serviceTypeData.constFind(serviceType);
0190     if (it != m_serviceTypeData.cend()) {
0191         return it.value().removedOffers.contains(service);
0192     }
0193     return false;
0194 }