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

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "partloader.h"
0009 
0010 #include "kparts_logging.h"
0011 
0012 #include <KConfigGroup>
0013 #include <KLocalizedString>
0014 #include <KPluginLoader>
0015 #include <KService>
0016 #include <KSharedConfig>
0017 #include <stack>
0018 
0019 #include <kparts_version.h> // TODO KF6 REMOVE
0020 #if KPARTS_VERSION <= QT_VERSION_CHECK(5, 900, 0)
0021 #include <KMimeTypeTrader>
0022 #include <KPluginInfo>
0023 #include <QMimeDatabase>
0024 #include <QMimeType>
0025 #endif
0026 
0027 // We still use desktop files for translated descriptions in keditfiletype,
0028 // and desktop file names then end up in mimeapps.list.
0029 // Alternatively, that KCM could be ported to read the descriptions from the JSON metadata?
0030 // KF6 TODO: at least make the KCM write out library names (into a different config file)
0031 // so we don't need to do the lookup here every time.
0032 static QString pluginForDesktopFile(const QString &desktopFile)
0033 {
0034     KService::Ptr service = KService::serviceByStorageId(desktopFile);
0035     if (!service) {
0036         qCDebug(KPARTSLOG) << "mimeapps.list specifies unknown service" << desktopFile;
0037         return {};
0038     }
0039     return service->library();
0040 }
0041 
0042 static QStringList partsFromUserPreference(const QString &mimeType)
0043 {
0044     auto config = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"));
0045     const QStringList desktopFiles = config->group(QStringLiteral("Added KDE Service Associations")).readXdgListEntry(mimeType);
0046     QStringList parts;
0047     parts.reserve(desktopFiles.count());
0048     for (const QString &desktopFile : desktopFiles) {
0049         const QString part = pluginForDesktopFile(desktopFile);
0050         if (!part.isEmpty()) {
0051             parts.append(part);
0052         }
0053     }
0054     return parts;
0055 }
0056 
0057 // A plugin can support N mimetypes. Pick the one that is closest to @parent in the inheritance tree
0058 // and return how far it is from that parent (0 = same mimetype, 1 = direct child, etc.)
0059 static int pluginDistanceToMimeType(const KPluginMetaData &md, const QString &parent)
0060 {
0061     QMimeDatabase db;
0062     auto distanceToMimeType = [&](const QString &mime) {
0063         if (mime == parent) {
0064             return 0;
0065         }
0066         const QStringList ancestors = db.mimeTypeForName(mime).allAncestors();
0067         const int dist = ancestors.indexOf(parent);
0068         return dist == -1 ? 50 : dist + 1;
0069     };
0070     const QStringList mimes = md.mimeTypes();
0071     int minDistance = 50;
0072     for (const QString &mime : mimes) {
0073         minDistance = std::min(minDistance, distanceToMimeType(mime));
0074     }
0075     return minDistance;
0076 }
0077 
0078 QVector<KPluginMetaData> KParts::PartLoader::partsForMimeType(const QString &mimeType)
0079 {
0080     auto supportsMime = [&](const KPluginMetaData &md) {
0081         return md.supportsMimeType(mimeType);
0082     };
0083     QVector<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/parts"), supportsMime);
0084 
0085 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0086     // KF5 compat code
0087 
0088     // I would compare library filenames, but KPluginMetaData::fileName looks like kf5/kparts/okteta and KService::library() is a full path
0089     // The user actually sees translated names, let's ensure those don't look duplicated in the list.
0090     auto isPluginForName = [](const QString &name) {
0091         return [name](const KPluginMetaData &plugin) {
0092             return plugin.name() == name;
0093         };
0094     };
0095 
0096     QT_WARNING_PUSH
0097     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0098     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0099     const KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("KParts/ReadOnlyPart"));
0100     for (const KService::Ptr &service : offers) {
0101         KPluginInfo info(service);
0102         if (info.isValid()) {
0103             if (std::find_if(plugins.cbegin(), plugins.cend(), isPluginForName(info.name())) == plugins.cend()) {
0104                 plugins.append(info.toMetaData());
0105             }
0106         }
0107     }
0108     QT_WARNING_POP
0109 #endif
0110 
0111     auto orderPredicate = [&](const KPluginMetaData &left, const KPluginMetaData &right) {
0112         // We filtered based on "supports mimetype", but this didn't order from most-specific to least-specific.
0113         const int leftDistance = pluginDistanceToMimeType(left, mimeType);
0114         const int rightDistance = pluginDistanceToMimeType(right, mimeType);
0115         if (leftDistance < rightDistance) {
0116             return true;
0117         }
0118         if (leftDistance > rightDistance) {
0119             return false;
0120         }
0121         // Plugins who support the same mimetype are then sorted by initial preference
0122         return left.initialPreference() > right.initialPreference();
0123     };
0124     std::sort(plugins.begin(), plugins.end(), orderPredicate);
0125 
0126     const QStringList userParts = partsFromUserPreference(mimeType);
0127     if (!userParts.isEmpty()) {
0128         // for (const KPluginMetaData &plugin : plugins) {
0129         //    qDebug() << "unsorted:" << plugin.fileName() << plugin.initialPreference();
0130         //}
0131         const auto defaultPlugins = plugins;
0132         plugins.clear();
0133         for (const QString &userPart : userParts) { // e.g. kf5/parts/gvpart
0134             auto matchesLibrary = [&](const KPluginMetaData &plugin) {
0135                 return plugin.fileName().contains(userPart);
0136             };
0137             auto it = std::find_if(defaultPlugins.begin(), defaultPlugins.end(), matchesLibrary);
0138             if (it != defaultPlugins.end()) {
0139                 plugins.push_back(*it);
0140             } else {
0141                 qCDebug(KPARTSLOG) << "Part not found" << userPart;
0142             }
0143         }
0144         // In case mimeapps.list lists "nothing good", append the default set to the end as fallback
0145         plugins += defaultPlugins;
0146     }
0147 
0148     // for (const KPluginMetaData &plugin : plugins) {
0149     //    qDebug() << plugin.fileName() << plugin.initialPreference();
0150     //}
0151     return plugins;
0152 }
0153 
0154 void KParts::PartLoader::Private::getErrorStrings(QString *errorString, QString *errorText, const QString &argument, ErrorType type)
0155 {
0156     switch (type) {
0157     case CouldNotLoadPlugin:
0158         *errorString = i18n("KPluginFactory could not load the plugin: %1", argument);
0159         *errorText = QStringLiteral("KPluginFactory could not load the plugin: %1").arg(argument);
0160         break;
0161     case NoPartFoundForMimeType:
0162         *errorString = i18n("No part was found for mimeType %1", argument);
0163         *errorText = QStringLiteral("No part was found for mimeType %1").arg(argument);
0164         break;
0165     case NoPartInstantiatedForMimeType:
0166         *errorString = i18n("No part could be instantiated for mimeType %1", argument);
0167         *errorText = QStringLiteral("No part could be instantiated for mimeType %1").arg(argument);
0168         break;
0169     default:
0170         qCWarning(KPARTSLOG) << "PartLoader::Private::getErrorStrings got unexpected error type" << type;
0171         break;
0172     }
0173 };
0174 
0175 #if KPARTS_BUILD_DEPRECATED_SINCE(5, 88)
0176 class KPluginFactoryHack : public KPluginFactory
0177 {
0178 public:
0179     QObject *create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args, const QString &keyword) override
0180     {
0181         return KPluginFactory::create(iface, parentWidget, parent, args, keyword);
0182     }
0183 };
0184 
0185 QObject *KParts::PartLoader::Private::createPartInstanceForMimeTypeHelper(const char *iface,
0186                                                                           const QString &mimeType,
0187                                                                           QWidget *parentWidget,
0188                                                                           QObject *parent,
0189                                                                           QString *error)
0190 {
0191     QT_WARNING_PUSH
0192     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0193     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0194     const QVector<KPluginMetaData> plugins = KParts::PartLoader::partsForMimeType(mimeType);
0195     for (const KPluginMetaData &plugin : plugins) {
0196         KPluginLoader pluginLoader(plugin.fileName());
0197         const QString pluginKeyword;
0198         KPluginFactory *factory = pluginLoader.factory();
0199         if (factory) {
0200             QObject *obj = static_cast<KPluginFactoryHack *>(factory)->create(iface, parentWidget, parent, QVariantList(), pluginKeyword);
0201             if (error) {
0202                 if (!obj) {
0203                     *error = i18n("The plugin '%1' does not provide an interface '%2' with keyword '%3'",
0204                                   plugin.fileName(),
0205                                   QString::fromLatin1(iface),
0206                                   pluginKeyword);
0207                 } else {
0208                     error->clear();
0209                 }
0210             }
0211             if (obj) {
0212                 return obj;
0213             }
0214         } else if (error) {
0215             *error = pluginLoader.errorString();
0216         }
0217         pluginLoader.unload();
0218     }
0219     if (error) {
0220         *error = i18n("No part was found for mimeType %1", mimeType);
0221     }
0222     return nullptr;
0223     QT_WARNING_POP
0224 }
0225 #endif