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"