File indexing completed on 2024-05-19 03:56:22

0001 /*
0002     This file is part of the KDE project
0003 
0004     SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>
0005     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kpluginmetadata.h"
0011 #include "kstaticpluginhelpers_p.h"
0012 
0013 #include "kcoreaddons_debug.h"
0014 #include "kjsonutils.h"
0015 #include <QCoreApplication>
0016 #include <QDir>
0017 #include <QDirIterator>
0018 #include <QFileInfo>
0019 #include <QJsonArray>
0020 #include <QJsonDocument>
0021 #include <QLocale>
0022 #include <QMimeDatabase>
0023 #include <QPluginLoader>
0024 #include <QStandardPaths>
0025 
0026 #include "kaboutdata.h"
0027 
0028 #include <optional>
0029 #include <unordered_map>
0030 
0031 using PluginCache = std::unordered_map<QString, std::vector<KPluginMetaData>>;
0032 Q_GLOBAL_STATIC(PluginCache, s_pluginNamespaceCache)
0033 
0034 class KPluginMetaDataPrivate : public QSharedData
0035 {
0036 public:
0037     KPluginMetaDataPrivate(const QJsonObject &obj, const QString &fileName, KPluginMetaData::KPluginMetaDataOptions options = {})
0038         : m_metaData(obj)
0039         , m_rootObj(obj.value(QLatin1String("KPlugin")).toObject())
0040         , m_fileName(fileName)
0041         , m_options(options)
0042     {
0043     }
0044     const QJsonObject m_metaData;
0045     const QJsonObject m_rootObj;
0046     // If we want to load a file, but it does not exist we want to keep the requested file name for logging
0047     QString m_requestedFileName;
0048     const QString m_fileName;
0049     const KPluginMetaData::KPluginMetaDataOptions m_options;
0050     std::optional<QStaticPlugin> staticPlugin = std::nullopt;
0051     // We determine this once and reuse the value. It can never change during the
0052     // lifetime of the KPluginMetaData object
0053     QString m_pluginId;
0054     qint64 m_lastQueriedTs = 0;
0055 
0056     static void forEachPlugin(const QString &directory, std::function<void(const QFileInfo &)> callback)
0057     {
0058         QStringList dirsToCheck;
0059 #ifdef Q_OS_ANDROID
0060         dirsToCheck << QCoreApplication::libraryPaths();
0061 #else
0062         if (QDir::isAbsolutePath(directory)) {
0063             dirsToCheck << directory;
0064         } else {
0065             dirsToCheck = QCoreApplication::libraryPaths();
0066             const QString appDirPath = QCoreApplication::applicationDirPath();
0067             dirsToCheck.removeOne(appDirPath);
0068             dirsToCheck.prepend(appDirPath);
0069 
0070             for (QString &libDir : dirsToCheck) {
0071                 libDir += QLatin1Char('/') + directory;
0072             }
0073         }
0074 #endif
0075 
0076         qCDebug(KCOREADDONS_DEBUG) << "Checking for plugins in" << dirsToCheck;
0077 
0078         for (const QString &dir : std::as_const(dirsToCheck)) {
0079             QDirIterator it(dir, QDir::Files);
0080             while (it.hasNext()) {
0081                 it.next();
0082 #ifdef Q_OS_ANDROID
0083                 QString prefix(QLatin1String("libplugins_") + QString(directory).replace(QLatin1Char('/'), QLatin1String("_")));
0084                 if (!prefix.endsWith(QLatin1Char('_'))) {
0085                     prefix.append(QLatin1Char('_'));
0086                 }
0087                 if (it.fileName().startsWith(prefix) && QLibrary::isLibrary(it.fileName())) {
0088 #else
0089                 if (QLibrary::isLibrary(it.fileName())) {
0090 #endif
0091                     callback(it.fileInfo());
0092                 }
0093             }
0094         }
0095     }
0096 
0097     struct StaticPluginLoadResult {
0098         QString fileName;
0099         QJsonObject metaData;
0100     };
0101     // This is only relevant in the findPlugins context and thus internal API.
0102     // If one has a static plugin from QPluginLoader::staticPlugins and does not
0103     // want it to have metadata, using KPluginMetaData makes no sense
0104     static KPluginMetaData
0105     ofStaticPlugin(const QString &pluginNamespace, const QString &fileName, KPluginMetaData::KPluginMetaDataOptions options, QStaticPlugin plugin)
0106     {
0107         QString pluginPath = pluginNamespace + u'/' + fileName;
0108         auto d = new KPluginMetaDataPrivate(plugin.metaData().value(QLatin1String("MetaData")).toObject(), pluginPath, options);
0109         d->staticPlugin = plugin;
0110         d->m_pluginId = fileName;
0111         KPluginMetaData data;
0112         data.d = d;
0113         return data;
0114     }
0115     static void pluginLoaderForPath(QPluginLoader &loader, const QString &path)
0116     {
0117         if (path.startsWith(QLatin1Char('/'))) { // Absolute path, use as it is
0118             loader.setFileName(path);
0119         } else {
0120             loader.setFileName(QCoreApplication::applicationDirPath() + QLatin1Char('/') + path);
0121             if (loader.fileName().isEmpty()) {
0122                 loader.setFileName(path);
0123             }
0124         }
0125     }
0126 
0127     static KPluginMetaDataPrivate *ofPath(const QString &path, KPluginMetaData::KPluginMetaDataOptions options)
0128     {
0129         QPluginLoader loader;
0130         pluginLoaderForPath(loader, path);
0131         if (loader.metaData().isEmpty()) {
0132             qCDebug(KCOREADDONS_DEBUG) << "no metadata found in" << loader.fileName() << loader.errorString();
0133         }
0134         auto ret = new KPluginMetaDataPrivate(loader.metaData().value(QLatin1String("MetaData")).toObject(), //
0135                                               QFileInfo(loader.fileName()).absoluteFilePath(),
0136                                               options);
0137         ret->m_requestedFileName = path;
0138         return ret;
0139     }
0140 };
0141 
0142 KPluginMetaData::KPluginMetaData()
0143     : d(new KPluginMetaDataPrivate(QJsonObject(), QString()))
0144 {
0145 }
0146 
0147 KPluginMetaData::KPluginMetaData(const KPluginMetaData &other)
0148     : d(other.d)
0149 {
0150 }
0151 
0152 KPluginMetaData &KPluginMetaData::operator=(const KPluginMetaData &other)
0153 {
0154     d = other.d;
0155     return *this;
0156 }
0157 
0158 KPluginMetaData::~KPluginMetaData() = default;
0159 
0160 KPluginMetaData::KPluginMetaData(const QString &pluginFile, KPluginMetaDataOptions options)
0161     : d(KPluginMetaDataPrivate::ofPath(pluginFile, options))
0162 {
0163     // passing QFileInfo an empty string gives the CWD, which is not what we want
0164     if (!d->m_fileName.isEmpty()) {
0165         d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
0166     }
0167 
0168     if (d->m_metaData.isEmpty() && !options.testFlags(KPluginMetaDataOption::AllowEmptyMetaData)) {
0169         qCDebug(KCOREADDONS_DEBUG) << "plugin metadata in" << pluginFile << "does not have a valid 'MetaData' object";
0170     }
0171     if (const QString id = d->m_rootObj[QLatin1String("Id")].toString(); !id.isEmpty()) {
0172         if (id != d->m_pluginId) {
0173             qWarning(KCOREADDONS_DEBUG) << "The plugin" << pluginFile
0174                                         << "explicitly states an Id in the embedded metadata, which is different from the one derived from the filename"
0175                                         << "The Id field from the KPlugin object in the metadata should be removed";
0176         } else {
0177             qInfo(KCOREADDONS_DEBUG) << "The plugin" << pluginFile << "explicitly states an 'Id' in the embedded metadata."
0178                                      << "This value should be removed, the resulting pluginId will not be affected by it";
0179         }
0180     }
0181 }
0182 
0183 KPluginMetaData::KPluginMetaData(const QPluginLoader &loader, KPluginMetaDataOptions options)
0184     : d(new KPluginMetaDataPrivate(loader.metaData().value(QLatin1String("MetaData")).toObject(), loader.fileName(), options))
0185 {
0186     if (!loader.fileName().isEmpty()) {
0187         d->m_pluginId = QFileInfo(loader.fileName()).completeBaseName();
0188     }
0189 }
0190 
0191 KPluginMetaData::KPluginMetaData(const QJsonObject &metaData, const QString &fileName)
0192     : d(new KPluginMetaDataPrivate(metaData, fileName))
0193 {
0194     auto nameFromMetaData = d->m_rootObj.constFind(QStringLiteral("Id"));
0195     if (nameFromMetaData != d->m_rootObj.constEnd()) {
0196         d->m_pluginId = nameFromMetaData.value().toString();
0197     }
0198     if (d->m_pluginId.isEmpty()) {
0199         d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
0200     }
0201 }
0202 
0203 KPluginMetaData KPluginMetaData::findPluginById(const QString &directory, const QString &pluginId, KPluginMetaDataOptions options)
0204 {
0205     QPluginLoader loader;
0206     const QString fileName = directory + QLatin1Char('/') + pluginId;
0207     KPluginMetaDataPrivate::pluginLoaderForPath(loader, fileName);
0208     if (loader.load()) {
0209         if (KPluginMetaData metaData(loader, options); metaData.isValid()) {
0210             return metaData;
0211         }
0212     }
0213 
0214     if (const auto staticOptional = KStaticPluginHelpers::findById(directory, pluginId)) {
0215         KPluginMetaData data = KPluginMetaDataPrivate::ofStaticPlugin(directory, pluginId, options, staticOptional.value());
0216         Q_ASSERT(data.fileName() == fileName);
0217         return data;
0218     }
0219 
0220     return KPluginMetaData{};
0221 }
0222 
0223 KPluginMetaData KPluginMetaData::fromJsonFile(const QString &file)
0224 {
0225     QFile f(file);
0226     bool b = f.open(QIODevice::ReadOnly);
0227     if (!b) {
0228         qCWarning(KCOREADDONS_DEBUG) << "Couldn't open" << file;
0229         return {};
0230     }
0231     QJsonParseError error;
0232     const QJsonObject metaData = QJsonDocument::fromJson(f.readAll(), &error).object();
0233     if (error.error) {
0234         qCWarning(KCOREADDONS_DEBUG) << "error parsing" << file << error.errorString();
0235     }
0236 
0237     return KPluginMetaData(metaData, QFileInfo(file).absoluteFilePath());
0238 }
0239 
0240 QJsonObject KPluginMetaData::rawData() const
0241 {
0242     return d->m_metaData;
0243 }
0244 
0245 QString KPluginMetaData::fileName() const
0246 {
0247     return d->m_fileName;
0248 }
0249 QList<KPluginMetaData>
0250 KPluginMetaData::findPlugins(const QString &directory, std::function<bool(const KPluginMetaData &)> filter, KPluginMetaDataOptions options)
0251 {
0252     QList<KPluginMetaData> ret;
0253     const auto staticPlugins = KStaticPluginHelpers::staticPlugins(directory);
0254     for (auto it = staticPlugins.begin(); it != staticPlugins.end(); ++it) {
0255         KPluginMetaData metaData = KPluginMetaDataPrivate::ofStaticPlugin(directory, it.key(), options, it.value());
0256         if (metaData.isValid()) {
0257             if (!filter || filter(metaData)) {
0258                 ret << metaData;
0259             }
0260         }
0261     }
0262     QSet<QString> addedPluginIds;
0263     const qint64 nowTs = QDateTime::currentMSecsSinceEpoch(); // For the initial load, stating all files is not needed
0264     const bool checkCache = options.testFlags(KPluginMetaData::CacheMetaData);
0265     std::vector<KPluginMetaData> &cache = (*s_pluginNamespaceCache)[directory];
0266     KPluginMetaDataPrivate::forEachPlugin(directory, [&](const QFileInfo &pluginInfo) {
0267         const QString pluginFile = pluginInfo.absoluteFilePath();
0268 
0269         KPluginMetaData metadata;
0270         if (checkCache) {
0271             const auto it = std::find_if(cache.begin(), cache.end(), [&pluginFile](const KPluginMetaData &data) {
0272                 return pluginFile == data.fileName();
0273             });
0274             bool isNew = it == cache.cend();
0275             if (!isNew) {
0276                 const qint64 lastQueried = (*it).d->m_lastQueriedTs;
0277                 Q_ASSERT(lastQueried > 0);
0278                 isNew = lastQueried < pluginInfo.lastModified().toMSecsSinceEpoch();
0279             }
0280             if (!isNew) {
0281                 metadata = *it;
0282             } else {
0283                 metadata = KPluginMetaData(pluginFile, options);
0284                 metadata.d->m_lastQueriedTs = nowTs;
0285                 cache.push_back(metadata);
0286             }
0287         } else {
0288             metadata = KPluginMetaData(pluginFile, options);
0289         }
0290         if (!metadata.isValid()) {
0291             qCDebug(KCOREADDONS_DEBUG) << pluginFile << "does not contain valid JSON metadata";
0292             return;
0293         }
0294         if (addedPluginIds.contains(metadata.pluginId())) {
0295             return;
0296         }
0297         if (filter && !filter(metadata)) {
0298             return;
0299         }
0300         addedPluginIds << metadata.pluginId();
0301         ret.append(metadata);
0302     });
0303     return ret;
0304 }
0305 
0306 bool KPluginMetaData::isValid() const
0307 {
0308     // it can be valid even if m_fileName is empty (as long as the plugin id is
0309     // set)
0310     return !pluginId().isEmpty() && (!d->m_metaData.isEmpty() || d->m_options.testFlags(AllowEmptyMetaData));
0311 }
0312 
0313 bool KPluginMetaData::isHidden() const
0314 {
0315     return d->m_rootObj[QLatin1String("Hidden")].toBool();
0316 }
0317 
0318 static inline void addPersonFromJson(const QJsonObject &obj, QList<KAboutPerson> *out)
0319 {
0320     KAboutPerson person = KAboutPerson::fromJSON(obj);
0321     if (person.name().isEmpty()) {
0322         qCWarning(KCOREADDONS_DEBUG) << "Invalid plugin metadata: Attempting to create a KAboutPerson from JSON without 'Name' property:" << obj;
0323         return;
0324     }
0325     out->append(person);
0326 }
0327 
0328 static QList<KAboutPerson> aboutPersonFromJSON(const QJsonValue &people)
0329 {
0330     QList<KAboutPerson> ret;
0331     if (people.isObject()) {
0332         // single author
0333         addPersonFromJson(people.toObject(), &ret);
0334     } else if (people.isArray()) {
0335         const QJsonArray peopleArray = people.toArray();
0336         for (const QJsonValue &val : peopleArray) {
0337             if (val.isObject()) {
0338                 addPersonFromJson(val.toObject(), &ret);
0339             }
0340         }
0341     }
0342     return ret;
0343 }
0344 
0345 QList<KAboutPerson> KPluginMetaData::authors() const
0346 {
0347     return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Authors")]);
0348 }
0349 
0350 QList<KAboutPerson> KPluginMetaData::translators() const
0351 {
0352     return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Translators")]);
0353 }
0354 
0355 QList<KAboutPerson> KPluginMetaData::otherContributors() const
0356 {
0357     return aboutPersonFromJSON(d->m_rootObj[QLatin1String("OtherContributors")]);
0358 }
0359 
0360 QString KPluginMetaData::category() const
0361 {
0362     return d->m_rootObj[QLatin1String("Category")].toString();
0363 }
0364 
0365 QString KPluginMetaData::description() const
0366 {
0367     return KJsonUtils::readTranslatedString(d->m_rootObj, QStringLiteral("Description"));
0368 }
0369 
0370 QString KPluginMetaData::iconName() const
0371 {
0372     return d->m_rootObj[QLatin1String("Icon")].toString();
0373 }
0374 
0375 QString KPluginMetaData::license() const
0376 {
0377     return d->m_rootObj[QLatin1String("License")].toString();
0378 }
0379 
0380 QString KPluginMetaData::licenseText() const
0381 {
0382     return KAboutLicense::byKeyword(license()).text();
0383 }
0384 
0385 QString KPluginMetaData::name() const
0386 {
0387     return KJsonUtils::readTranslatedString(d->m_rootObj, QStringLiteral("Name"));
0388 }
0389 
0390 QString KPluginMetaData::copyrightText() const
0391 {
0392     return KJsonUtils::readTranslatedString(d->m_rootObj, QStringLiteral("Copyright"));
0393 }
0394 
0395 QString KPluginMetaData::pluginId() const
0396 {
0397     return d->m_pluginId;
0398 }
0399 
0400 QString KPluginMetaData::version() const
0401 {
0402     return d->m_rootObj[QLatin1String("Version")].toString();
0403 }
0404 
0405 QString KPluginMetaData::website() const
0406 {
0407     return d->m_rootObj[QLatin1String("Website")].toString();
0408 }
0409 
0410 QString KPluginMetaData::bugReportUrl() const
0411 {
0412     return d->m_rootObj[QLatin1String("BugReportUrl")].toString();
0413 }
0414 
0415 QStringList KPluginMetaData::mimeTypes() const
0416 {
0417     return d->m_rootObj[QLatin1String("MimeTypes")].toVariant().toStringList();
0418 }
0419 
0420 bool KPluginMetaData::supportsMimeType(const QString &mimeType) const
0421 {
0422     // Check for exact matches first. This can delay parsing the full MIME
0423     // database until later and noticeably speed up application startup on
0424     // slower systems.
0425     const QStringList mimes = mimeTypes();
0426     if (mimes.contains(mimeType)) {
0427         return true;
0428     }
0429 
0430     // Now check for MIME type inheritance to find non-exact matches:
0431     QMimeDatabase db;
0432     const QMimeType mime = db.mimeTypeForName(mimeType);
0433     if (!mime.isValid()) {
0434         return false;
0435     }
0436 
0437     return std::any_of(mimes.begin(), mimes.end(), [&](const QString &supportedMimeName) {
0438         return mime.inherits(supportedMimeName);
0439     });
0440 }
0441 
0442 QStringList KPluginMetaData::formFactors() const
0443 {
0444     return d->m_rootObj.value(QLatin1String("FormFactors")).toVariant().toStringList();
0445 }
0446 
0447 bool KPluginMetaData::isEnabledByDefault() const
0448 {
0449     const QLatin1String key("EnabledByDefault");
0450     const QJsonValue val = d->m_rootObj[key];
0451     if (val.isBool()) {
0452         return val.toBool();
0453     } else if (val.isString()) {
0454         qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be boolean, but it was a string";
0455         return val.toString() == QLatin1String("true");
0456     }
0457     return false;
0458 }
0459 
0460 QString KPluginMetaData::value(const QString &key, const QString &defaultValue) const
0461 {
0462     const QJsonValue value = d->m_metaData.value(key);
0463     if (value.isString()) {
0464         return value.toString(defaultValue);
0465     } else if (value.isArray()) {
0466         qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is an array";
0467         return value.toVariant().toStringList().join(QChar::fromLatin1(','));
0468     } else if (value.isBool()) {
0469         qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is a bool";
0470         return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
0471     }
0472     return defaultValue;
0473 }
0474 
0475 bool KPluginMetaData::value(const QString &key, bool defaultValue) const
0476 {
0477     const QJsonValue value = d->m_metaData.value(key);
0478     if (value.isBool()) {
0479         return value.toBool();
0480     } else if (value.isString()) {
0481         return value.toString() == QLatin1String("true");
0482     } else {
0483         return defaultValue;
0484     }
0485 }
0486 
0487 int KPluginMetaData::value(const QString &key, int defaultValue) const
0488 {
0489     const QJsonValue value = d->m_metaData.value(key);
0490     if (value.isDouble()) {
0491         return value.toInt();
0492     } else if (value.isString()) {
0493         const QString intString = value.toString();
0494         bool ok;
0495         int convertedIntValue = intString.toInt(&ok);
0496         if (ok) {
0497             return convertedIntValue;
0498         } else {
0499             qCWarning(KCOREADDONS_DEBUG) << "Expected" << key << "to be an int, instead" << intString << "was specified in the JSON metadata" << d->m_fileName;
0500             return defaultValue;
0501         }
0502     } else {
0503         return defaultValue;
0504     }
0505 }
0506 QStringList KPluginMetaData::value(const QString &key, const QStringList &defaultValue) const
0507 {
0508     const QJsonValue value = d->m_metaData.value(key);
0509     if (value.isUndefined() || value.isNull()) {
0510         return defaultValue;
0511     } else if (value.isObject()) {
0512         qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list, instead an object was specified in" << d->m_fileName;
0513         return defaultValue;
0514     } else if (value.isArray()) {
0515         return value.toVariant().toStringList();
0516     } else {
0517         const QString asString = value.isString() ? value.toString() : value.toVariant().toString();
0518         if (asString.isEmpty()) {
0519             return defaultValue;
0520         }
0521         qCDebug(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list in" << d->m_fileName
0522                                    << "Treating it as a list with a single entry:" << asString;
0523         return QStringList(asString);
0524     }
0525 }
0526 
0527 bool KPluginMetaData::operator==(const KPluginMetaData &other) const
0528 {
0529     return d->m_fileName == other.d->m_fileName && d->m_metaData == other.d->m_metaData;
0530 }
0531 
0532 bool KPluginMetaData::isStaticPlugin() const
0533 {
0534     return d->staticPlugin.has_value();
0535 }
0536 
0537 QString KPluginMetaData::requestedFileName() const
0538 {
0539     return d->m_requestedFileName;
0540 }
0541 
0542 QStaticPlugin KPluginMetaData::staticPlugin() const
0543 {
0544     Q_ASSERT(d);
0545     Q_ASSERT(d->staticPlugin.has_value());
0546     return d->staticPlugin.value();
0547 }
0548 
0549 QDebug operator<<(QDebug debug, const KPluginMetaData &metaData)
0550 {
0551     QDebugStateSaver saver(debug);
0552     debug.nospace() << "KPluginMetaData(pluginId:" << metaData.pluginId() << ", fileName: " << metaData.fileName() << ')';
0553     return debug;
0554 }
0555 
0556 #include "moc_kpluginmetadata.cpp"