File indexing completed on 2024-05-19 05:47:23

0001 /*  This file is part of the KDE libraries
0002  *  Copyright 2008  David Faure  <faure@kde.org>
0003  *
0004  *  This library is free software; you can redistribute it and/or modify
0005  *  it under the terms of the GNU Lesser General Public License as published by
0006  *  the Free Software Foundation; either version 2 of the License or ( at
0007  *  your option ) version 3 or, at the discretion of KDE e.V. ( which shall
0008  *  act as a proxy as in section 14 of the GPLv3 ), any later version.
0009  *
0010  *  This library is distributed in the hope that it will be useful,
0011  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013  *  Library General Public License for more details.
0014  *
0015  *  You should have received a copy of the GNU Lesser General Public License
0016  *  along with this library; see the file COPYING.LIB.  If not, write to
0017  *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018  *  Boston, MA 02110-1301, USA.
0019  */
0020 
0021 #include "kmimeassociations_p.h"
0022 #include <kservice.h>
0023 #include <kservicefactory_p.h>
0024 #include <kconfiggroup.h>
0025 #include <kconfig.h>
0026 #include <QDebug>
0027 #include <QFile>
0028 #include <qstandardpaths.h>
0029 #include <qmimedatabase.h>
0030 #include "sycocadebug.h"
0031 
0032 KMimeAssociations::KMimeAssociations(KOfferHash &offerHash, KServiceFactory *serviceFactory)
0033     : m_offerHash(offerHash), m_serviceFactory(serviceFactory)
0034 {
0035 }
0036 
0037 /*
0038 
0039 The goal of this class is to parse mimeapps.list files, which are used to
0040 let users configure the application-mimetype associations.
0041 
0042 Example file:
0043 
0044 [Added Associations]
0045 text/plain=kate.desktop;
0046 
0047 [Removed Associations]
0048 text/plain=gnome-gedit.desktop;gnu-emacs.desktop;
0049 
0050 */
0051 
0052 void KMimeAssociations::parseAllMimeAppsList()
0053 {
0054     QStringList mimeappsFileNames;
0055     // make the list of possible filenames from the spec ($desktop-mimeapps.list, then mimeapps.list)
0056     const QString desktops = QString::fromLocal8Bit(qgetenv("XDG_CURRENT_DESKTOP"));
0057     const auto list = desktops.split(QLatin1Char(':'), QString::SkipEmptyParts);
0058     for (const QString &desktop : list) {
0059         mimeappsFileNames.append(desktop.toLower() + QLatin1String("-mimeapps.list"));
0060     }
0061     mimeappsFileNames.append(QStringLiteral("mimeapps.list"));
0062     // list the dirs in the order of the spec (XDG_CONFIG_HOME, XDG_CONFIG_DIRS, XDG_DATA_HOME, XDG_DATA_DIRS)
0063     const QStringList mimeappsDirs = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation)
0064                                     + QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
0065     QStringList mimeappsFiles;
0066     // collect existing files
0067     for (const QString &dir : mimeappsDirs) {
0068         for (const QString &file : qAsConst(mimeappsFileNames)) {
0069             const QString filePath = dir + QLatin1Char('/') + file;
0070             if (QFile::exists(filePath)) {
0071                 mimeappsFiles.append(filePath);
0072             }
0073         }
0074     }
0075     //qDebug() << "FILE LIST:" << mimeappsFiles;
0076 
0077     int basePreference = 1000; // start high :)
0078     QListIterator<QString> mimeappsIter(mimeappsFiles);
0079     mimeappsIter.toBack();
0080     while (mimeappsIter.hasPrevious()) { // global first, then local.
0081         const QString mimeappsFile = mimeappsIter.previous();
0082         //qDebug() << "Parsing" << mimeappsFile;
0083         parseMimeAppsList(mimeappsFile, basePreference);
0084         basePreference += 50;
0085     }
0086 }
0087 
0088 void KMimeAssociations::parseMimeAppsList(const QString &file, int basePreference)
0089 {
0090     KConfig profile(file, KConfig::SimpleConfig);
0091     if (file.endsWith(QLatin1String("/mimeapps.list"))) { // not for $desktop-mimeapps.list
0092         parseAddedAssociations(KConfigGroup(&profile, "Added Associations"), file, basePreference);
0093         parseRemovedAssociations(KConfigGroup(&profile, "Removed Associations"), file);
0094 
0095         // KDE extension for parts and plugins, see settings/filetypes/mimetypedata.cpp
0096         parseAddedAssociations(KConfigGroup(&profile, "Added KDE Service Associations"), file, basePreference);
0097         parseRemovedAssociations(KConfigGroup(&profile, "Removed KDE Service Associations"), file);
0098     }
0099 
0100     // TODO "Default Applications" is a separate query and a separate algorithm, says the spec.
0101     // For now this is better than nothing though.
0102     parseAddedAssociations(KConfigGroup(&profile, "Default Applications"), file, basePreference);
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 mimeType" << 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, pService->allowAsDefault()));
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         //qDebug() << service->entryPath() << "already in" << serviceType;
0161         // This happens when mimeapps.list mentions a service (to make it preferred)
0162         // Update initialPreference to qMax(existing offer, new offer)
0163         QMutableListIterator<KServiceOffer> sfit(data.offers);
0164         while (sfit.hasNext()) {
0165             if (sfit.next().service() == service) { // we can compare KService::Ptrs because they are from the memory hash
0166                 sfit.value().setPreference(qMax(sfit.value().preference(), offer.preference()));
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     QMutableListIterator<KServiceOffer> sfit(data.offers);
0178     while (sfit.hasNext()) {
0179         if (sfit.next().service()->storageId() == service->storageId()) {
0180             sfit.remove();
0181         }
0182     }
0183 }
0184 
0185 bool KOfferHash::hasRemovedOffer(const QString &serviceType, const KService::Ptr &service) const
0186 {
0187     QHash<QString, ServiceTypeOffersData>::const_iterator it = m_serviceTypeData.find(serviceType);
0188     if (it != m_serviceTypeData.end()) {
0189         return (*it).removedOffers.contains(service);
0190     }
0191     return false;
0192 }