1/*
2 This file is part of the KDE project
3
4 SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>
5 SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kpluginmetadata.h"
11#include "kstaticpluginhelpers_p.h"
12
13#include "kcoreaddons_debug.h"
14#include "kjsonutils.h"
15#include <QCoreApplication>
16#include <QDir>
17#include <QDirIterator>
18#include <QFileInfo>
19#include <QJsonArray>
20#include <QJsonDocument>
21#include <QLocale>
22#include <QMimeDatabase>
23#include <QPluginLoader>
24#include <QStandardPaths>
25
26#include "kaboutdata.h"
27
28#include <optional>
29#include <unordered_map>
30
31using PluginCache = std::unordered_map<QString, std::vector<KPluginMetaData>>;
32Q_GLOBAL_STATIC(PluginCache, s_pluginNamespaceCache)
33
34class KPluginMetaDataPrivate : public QSharedData
35{
36public:
37 KPluginMetaDataPrivate(const QJsonObject &obj, const QString &fileName, KPluginMetaData::KPluginMetaDataOptions options = {})
38 : m_metaData(obj)
39 , m_rootObj(obj.value(key: QLatin1String("KPlugin")).toObject())
40 , m_fileName(fileName)
41 , m_options(options)
42 {
43 }
44 const QJsonObject m_metaData;
45 const QJsonObject m_rootObj;
46 // If we want to load a file, but it does not exist we want to keep the requested file name for logging
47 QString m_requestedFileName;
48 const QString m_fileName;
49 const KPluginMetaData::KPluginMetaDataOptions m_options;
50 std::optional<QStaticPlugin> staticPlugin = std::nullopt;
51 // We determine this once and reuse the value. It can never change during the
52 // lifetime of the KPluginMetaData object
53 QString m_pluginId;
54 qint64 m_lastQueriedTs = 0;
55
56 static void forEachPlugin(const QString &directory, std::function<void(const QFileInfo &)> callback)
57 {
58 QStringList dirsToCheck;
59#ifdef Q_OS_ANDROID
60 dirsToCheck << QCoreApplication::libraryPaths();
61#else
62 if (QDir::isAbsolutePath(path: directory)) {
63 dirsToCheck << directory;
64 } else {
65 dirsToCheck = QCoreApplication::libraryPaths();
66 const QString appDirPath = QCoreApplication::applicationDirPath();
67 dirsToCheck.removeOne(t: appDirPath);
68 dirsToCheck.prepend(t: appDirPath);
69
70 for (QString &libDir : dirsToCheck) {
71 libDir += QLatin1Char('/') + directory;
72 }
73 }
74#endif
75
76 qCDebug(KCOREADDONS_DEBUG) << "Checking for plugins in" << dirsToCheck;
77
78 for (const QString &dir : std::as_const(t&: dirsToCheck)) {
79 QDirIterator it(dir, QDir::Files);
80 while (it.hasNext()) {
81 it.next();
82#ifdef Q_OS_ANDROID
83 QString prefix(QLatin1String("libplugins_") + QString(directory).replace(QLatin1Char('/'), QLatin1String("_")));
84 if (!prefix.endsWith(QLatin1Char('_'))) {
85 prefix.append(QLatin1Char('_'));
86 }
87 if (it.fileName().startsWith(prefix) && QLibrary::isLibrary(it.fileName())) {
88#else
89 if (QLibrary::isLibrary(fileName: it.fileName())) {
90#endif
91 callback(it.fileInfo());
92 }
93 }
94 }
95 }
96
97 struct StaticPluginLoadResult {
98 QString fileName;
99 QJsonObject metaData;
100 };
101 // This is only relevant in the findPlugins context and thus internal API.
102 // If one has a static plugin from QPluginLoader::staticPlugins and does not
103 // want it to have metadata, using KPluginMetaData makes no sense
104 static KPluginMetaData
105 ofStaticPlugin(const QString &pluginNamespace, const QString &fileName, KPluginMetaData::KPluginMetaDataOptions options, QStaticPlugin plugin)
106 {
107 QString pluginPath = pluginNamespace + u'/' + fileName;
108 auto d = new KPluginMetaDataPrivate(plugin.metaData().value(key: QLatin1String("MetaData")).toObject(), pluginPath, options);
109 d->staticPlugin = plugin;
110 d->m_pluginId = fileName;
111 KPluginMetaData data;
112 data.d = d;
113 return data;
114 }
115 static void pluginLoaderForPath(QPluginLoader &loader, const QString &path)
116 {
117 if (path.startsWith(c: QLatin1Char('/'))) { // Absolute path, use as it is
118 loader.setFileName(path);
119 } else {
120 loader.setFileName(QCoreApplication::applicationDirPath() + QLatin1Char('/') + path);
121 if (loader.fileName().isEmpty()) {
122 loader.setFileName(path);
123 }
124 }
125 }
126
127 static KPluginMetaDataPrivate *ofPath(const QString &path, KPluginMetaData::KPluginMetaDataOptions options)
128 {
129 QPluginLoader loader;
130 pluginLoaderForPath(loader, path);
131 if (loader.metaData().isEmpty()) {
132 qCDebug(KCOREADDONS_DEBUG) << "no metadata found in" << loader.fileName() << loader.errorString();
133 }
134 auto ret = new KPluginMetaDataPrivate(loader.metaData().value(key: QLatin1String("MetaData")).toObject(), //
135 QFileInfo(loader.fileName()).absoluteFilePath(),
136 options);
137 ret->m_requestedFileName = path;
138 return ret;
139 }
140};
141
142KPluginMetaData::KPluginMetaData()
143 : d(new KPluginMetaDataPrivate(QJsonObject(), QString()))
144{
145}
146
147KPluginMetaData::KPluginMetaData(const KPluginMetaData &other)
148 : d(other.d)
149{
150}
151
152KPluginMetaData &KPluginMetaData::operator=(const KPluginMetaData &other)
153{
154 d = other.d;
155 return *this;
156}
157
158KPluginMetaData::~KPluginMetaData() = default;
159
160KPluginMetaData::KPluginMetaData(const QString &pluginFile, KPluginMetaDataOptions options)
161 : d(KPluginMetaDataPrivate::ofPath(path: pluginFile, options))
162{
163 // passing QFileInfo an empty string gives the CWD, which is not what we want
164 if (!d->m_fileName.isEmpty()) {
165 d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
166 }
167
168 if (d->m_metaData.isEmpty() && !options.testFlags(flags: KPluginMetaDataOption::AllowEmptyMetaData)) {
169 qCDebug(KCOREADDONS_DEBUG) << "plugin metadata in" << pluginFile << "does not have a valid 'MetaData' object";
170 }
171 if (const QString id = d->m_rootObj[QLatin1String("Id")].toString(); !id.isEmpty()) {
172 if (id != d->m_pluginId) {
173 qWarning(catFunc: KCOREADDONS_DEBUG) << "The plugin" << pluginFile
174 << "explicitly states an Id in the embedded metadata, which is different from the one derived from the filename"
175 << "The Id field from the KPlugin object in the metadata should be removed";
176 } else {
177 qInfo(catFunc: KCOREADDONS_DEBUG) << "The plugin" << pluginFile << "explicitly states an 'Id' in the embedded metadata."
178 << "This value should be removed, the resulting pluginId will not be affected by it";
179 }
180 }
181}
182
183KPluginMetaData::KPluginMetaData(const QPluginLoader &loader, KPluginMetaDataOptions options)
184 : d(new KPluginMetaDataPrivate(loader.metaData().value(key: QLatin1String("MetaData")).toObject(), loader.fileName(), options))
185{
186 if (!loader.fileName().isEmpty()) {
187 d->m_pluginId = QFileInfo(loader.fileName()).completeBaseName();
188 }
189}
190
191KPluginMetaData::KPluginMetaData(const QJsonObject &metaData, const QString &fileName)
192 : d(new KPluginMetaDataPrivate(metaData, fileName))
193{
194 auto nameFromMetaData = d->m_rootObj.constFind(QStringLiteral("Id"));
195 if (nameFromMetaData != d->m_rootObj.constEnd()) {
196 d->m_pluginId = nameFromMetaData.value().toString();
197 }
198 if (d->m_pluginId.isEmpty()) {
199 d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
200 }
201}
202
203KPluginMetaData KPluginMetaData::findPluginById(const QString &directory, const QString &pluginId, KPluginMetaDataOptions options)
204{
205 QPluginLoader loader;
206 const QString fileName = directory + QLatin1Char('/') + pluginId;
207 KPluginMetaDataPrivate::pluginLoaderForPath(loader, path: fileName);
208 if (loader.load()) {
209 if (KPluginMetaData metaData(loader, options); metaData.isValid()) {
210 return metaData;
211 }
212 }
213
214 if (const auto staticOptional = KStaticPluginHelpers::findById(directory, pluginId)) {
215 KPluginMetaData data = KPluginMetaDataPrivate::ofStaticPlugin(pluginNamespace: directory, fileName: pluginId, options, plugin: staticOptional.value());
216 Q_ASSERT(data.fileName() == fileName);
217 return data;
218 }
219
220 return KPluginMetaData{};
221}
222
223KPluginMetaData KPluginMetaData::fromJsonFile(const QString &file)
224{
225 QFile f(file);
226 bool b = f.open(flags: QIODevice::ReadOnly);
227 if (!b) {
228 qCWarning(KCOREADDONS_DEBUG) << "Couldn't open" << file;
229 return {};
230 }
231 QJsonParseError error;
232 const QJsonObject metaData = QJsonDocument::fromJson(json: f.readAll(), error: &error).object();
233 if (error.error) {
234 qCWarning(KCOREADDONS_DEBUG) << "error parsing" << file << error.errorString();
235 }
236
237 return KPluginMetaData(metaData, QFileInfo(file).absoluteFilePath());
238}
239
240QJsonObject KPluginMetaData::rawData() const
241{
242 return d->m_metaData;
243}
244
245QString KPluginMetaData::fileName() const
246{
247 return d->m_fileName;
248}
249QList<KPluginMetaData>
250KPluginMetaData::findPlugins(const QString &directory, std::function<bool(const KPluginMetaData &)> filter, KPluginMetaDataOptions options)
251{
252 QList<KPluginMetaData> ret;
253 const auto staticPlugins = KStaticPluginHelpers::staticPlugins(directory);
254 for (auto it = staticPlugins.begin(); it != staticPlugins.end(); ++it) {
255 KPluginMetaData metaData = KPluginMetaDataPrivate::ofStaticPlugin(pluginNamespace: directory, fileName: it.key(), options, plugin: it.value());
256 if (metaData.isValid()) {
257 if (!filter || filter(metaData)) {
258 ret << metaData;
259 }
260 }
261 }
262 QSet<QString> addedPluginIds;
263 const qint64 nowTs = QDateTime::currentMSecsSinceEpoch(); // For the initial load, stating all files is not needed
264 const bool checkCache = options.testFlags(flags: KPluginMetaData::CacheMetaData);
265 std::vector<KPluginMetaData> &cache = (*s_pluginNamespaceCache)[directory];
266 KPluginMetaDataPrivate::forEachPlugin(directory, callback: [&](const QFileInfo &pluginInfo) {
267 const QString pluginFile = pluginInfo.absoluteFilePath();
268
269 KPluginMetaData metadata;
270 if (checkCache) {
271 const auto it = std::find_if(first: cache.begin(), last: cache.end(), pred: [&pluginFile](const KPluginMetaData &data) {
272 return pluginFile == data.fileName();
273 });
274 bool isNew = it == cache.cend();
275 if (!isNew) {
276 const qint64 lastQueried = (*it).d->m_lastQueriedTs;
277 Q_ASSERT(lastQueried > 0);
278 isNew = lastQueried < pluginInfo.lastModified().toMSecsSinceEpoch();
279 }
280 if (!isNew) {
281 metadata = *it;
282 } else {
283 metadata = KPluginMetaData(pluginFile, options);
284 metadata.d->m_lastQueriedTs = nowTs;
285 cache.push_back(x: metadata);
286 }
287 } else {
288 metadata = KPluginMetaData(pluginFile, options);
289 }
290 if (!metadata.isValid()) {
291 qCDebug(KCOREADDONS_DEBUG) << pluginFile << "does not contain valid JSON metadata";
292 return;
293 }
294 if (addedPluginIds.contains(value: metadata.pluginId())) {
295 return;
296 }
297 if (filter && !filter(metadata)) {
298 return;
299 }
300 addedPluginIds << metadata.pluginId();
301 ret.append(t: metadata);
302 });
303 return ret;
304}
305
306bool KPluginMetaData::isValid() const
307{
308 // it can be valid even if m_fileName is empty (as long as the plugin id is
309 // set)
310 return !pluginId().isEmpty() && (!d->m_metaData.isEmpty() || d->m_options.testFlags(flags: AllowEmptyMetaData));
311}
312
313bool KPluginMetaData::isHidden() const
314{
315 return d->m_rootObj[QLatin1String("Hidden")].toBool();
316}
317
318static inline void addPersonFromJson(const QJsonObject &obj, QList<KAboutPerson> *out)
319{
320 KAboutPerson person = KAboutPerson::fromJSON(obj);
321 if (person.name().isEmpty()) {
322 qCWarning(KCOREADDONS_DEBUG) << "Invalid plugin metadata: Attempting to create a KAboutPerson from JSON without 'Name' property:" << obj;
323 return;
324 }
325 out->append(t: person);
326}
327
328static QList<KAboutPerson> aboutPersonFromJSON(const QJsonValue &people)
329{
330 QList<KAboutPerson> ret;
331 if (people.isObject()) {
332 // single author
333 addPersonFromJson(obj: people.toObject(), out: &ret);
334 } else if (people.isArray()) {
335 const QJsonArray peopleArray = people.toArray();
336 for (const QJsonValue &val : peopleArray) {
337 if (val.isObject()) {
338 addPersonFromJson(obj: val.toObject(), out: &ret);
339 }
340 }
341 }
342 return ret;
343}
344
345QList<KAboutPerson> KPluginMetaData::authors() const
346{
347 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("Authors")]);
348}
349
350QList<KAboutPerson> KPluginMetaData::translators() const
351{
352 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("Translators")]);
353}
354
355QList<KAboutPerson> KPluginMetaData::otherContributors() const
356{
357 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("OtherContributors")]);
358}
359
360QString KPluginMetaData::category() const
361{
362 return d->m_rootObj[QLatin1String("Category")].toString();
363}
364
365QString KPluginMetaData::description() const
366{
367 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Description"));
368}
369
370QString KPluginMetaData::iconName() const
371{
372 return d->m_rootObj[QLatin1String("Icon")].toString();
373}
374
375QString KPluginMetaData::license() const
376{
377 return d->m_rootObj[QLatin1String("License")].toString();
378}
379
380QString KPluginMetaData::licenseText() const
381{
382 return KAboutLicense::byKeyword(keyword: license()).text();
383}
384
385QString KPluginMetaData::name() const
386{
387 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Name"));
388}
389
390QString KPluginMetaData::copyrightText() const
391{
392 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Copyright"));
393}
394
395QString KPluginMetaData::pluginId() const
396{
397 return d->m_pluginId;
398}
399
400QString KPluginMetaData::version() const
401{
402 return d->m_rootObj[QLatin1String("Version")].toString();
403}
404
405QString KPluginMetaData::website() const
406{
407 return d->m_rootObj[QLatin1String("Website")].toString();
408}
409
410QString KPluginMetaData::bugReportUrl() const
411{
412 return d->m_rootObj[QLatin1String("BugReportUrl")].toString();
413}
414
415QStringList KPluginMetaData::mimeTypes() const
416{
417 return d->m_rootObj[QLatin1String("MimeTypes")].toVariant().toStringList();
418}
419
420bool KPluginMetaData::supportsMimeType(const QString &mimeType) const
421{
422 // Check for exact matches first. This can delay parsing the full MIME
423 // database until later and noticeably speed up application startup on
424 // slower systems.
425 const QStringList mimes = mimeTypes();
426 if (mimes.contains(str: mimeType)) {
427 return true;
428 }
429
430 // Now check for MIME type inheritance to find non-exact matches:
431 QMimeDatabase db;
432 const QMimeType mime = db.mimeTypeForName(nameOrAlias: mimeType);
433 if (!mime.isValid()) {
434 return false;
435 }
436
437 return std::any_of(first: mimes.begin(), last: mimes.end(), pred: [&](const QString &supportedMimeName) {
438 return mime.inherits(mimeTypeName: supportedMimeName);
439 });
440}
441
442QStringList KPluginMetaData::formFactors() const
443{
444 return d->m_rootObj.value(key: QLatin1String("FormFactors")).toVariant().toStringList();
445}
446
447bool KPluginMetaData::isEnabledByDefault() const
448{
449 const QLatin1String key("EnabledByDefault");
450 const QJsonValue val = d->m_rootObj[key];
451 if (val.isBool()) {
452 return val.toBool();
453 } else if (val.isString()) {
454 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be boolean, but it was a string";
455 return val.toString() == QLatin1String("true");
456 }
457 return false;
458}
459
460QString KPluginMetaData::value(const QString &key, const QString &defaultValue) const
461{
462 const QJsonValue value = d->m_metaData.value(key);
463 if (value.isString()) {
464 return value.toString(defaultValue);
465 } else if (value.isArray()) {
466 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is an array";
467 return value.toVariant().toStringList().join(sep: QChar::fromLatin1(c: ','));
468 } else if (value.isBool()) {
469 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is a bool";
470 return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
471 }
472 return defaultValue;
473}
474
475bool KPluginMetaData::value(const QString &key, bool defaultValue) const
476{
477 const QJsonValue value = d->m_metaData.value(key);
478 if (value.isBool()) {
479 return value.toBool();
480 } else if (value.isString()) {
481 return value.toString() == QLatin1String("true");
482 } else {
483 return defaultValue;
484 }
485}
486
487int KPluginMetaData::value(const QString &key, int defaultValue) const
488{
489 const QJsonValue value = d->m_metaData.value(key);
490 if (value.isDouble()) {
491 return value.toInt();
492 } else if (value.isString()) {
493 const QString intString = value.toString();
494 bool ok;
495 int convertedIntValue = intString.toInt(ok: &ok);
496 if (ok) {
497 return convertedIntValue;
498 } else {
499 qCWarning(KCOREADDONS_DEBUG) << "Expected" << key << "to be an int, instead" << intString << "was specified in the JSON metadata" << d->m_fileName;
500 return defaultValue;
501 }
502 } else {
503 return defaultValue;
504 }
505}
506QStringList KPluginMetaData::value(const QString &key, const QStringList &defaultValue) const
507{
508 const QJsonValue value = d->m_metaData.value(key);
509 if (value.isUndefined() || value.isNull()) {
510 return defaultValue;
511 } else if (value.isObject()) {
512 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list, instead an object was specified in" << d->m_fileName;
513 return defaultValue;
514 } else if (value.isArray()) {
515 return value.toVariant().toStringList();
516 } else {
517 const QString asString = value.isString() ? value.toString() : value.toVariant().toString();
518 if (asString.isEmpty()) {
519 return defaultValue;
520 }
521 qCDebug(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list in" << d->m_fileName
522 << "Treating it as a list with a single entry:" << asString;
523 return QStringList(asString);
524 }
525}
526
527bool KPluginMetaData::operator==(const KPluginMetaData &other) const
528{
529 return d->m_fileName == other.d->m_fileName && d->m_metaData == other.d->m_metaData;
530}
531
532bool KPluginMetaData::isStaticPlugin() const
533{
534 return d->staticPlugin.has_value();
535}
536
537QString KPluginMetaData::requestedFileName() const
538{
539 return d->m_requestedFileName;
540}
541
542QStaticPlugin KPluginMetaData::staticPlugin() const
543{
544 Q_ASSERT(d);
545 Q_ASSERT(d->staticPlugin.has_value());
546 return d->staticPlugin.value();
547}
548
549QDebug operator<<(QDebug debug, const KPluginMetaData &metaData)
550{
551 QDebugStateSaver saver(debug);
552 debug.nospace() << "KPluginMetaData(pluginId:" << metaData.pluginId() << ", fileName: " << metaData.fileName() << ')';
553 return debug;
554}
555
556#include "moc_kpluginmetadata.cpp"
557

source code of kcoreaddons/src/lib/plugin/kpluginmetadata.cpp