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
132 const QJsonObject metaData = loader.metaData();
133
134 if (metaData.isEmpty()) {
135 qCDebug(KCOREADDONS_DEBUG) << "no metadata found in" << loader.fileName() << loader.errorString();
136 }
137 auto ret = new KPluginMetaDataPrivate(metaData.value(key: QLatin1String("MetaData")).toObject(), //
138 QFileInfo(loader.fileName()).absoluteFilePath(),
139 options);
140 ret->m_requestedFileName = path;
141 return ret;
142 }
143};
144
145KPluginMetaData::KPluginMetaData()
146 : d(new KPluginMetaDataPrivate(QJsonObject(), QString()))
147{
148}
149
150KPluginMetaData::KPluginMetaData(const KPluginMetaData &other)
151 : d(other.d)
152{
153}
154
155KPluginMetaData &KPluginMetaData::operator=(const KPluginMetaData &other)
156{
157 d = other.d;
158 return *this;
159}
160
161KPluginMetaData::~KPluginMetaData() = default;
162
163KPluginMetaData::KPluginMetaData(const QString &pluginFile, KPluginMetaDataOptions options)
164 : d(KPluginMetaDataPrivate::ofPath(path: pluginFile, options))
165{
166 // passing QFileInfo an empty string gives the CWD, which is not what we want
167 if (!d->m_fileName.isEmpty()) {
168 d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
169 }
170
171 if (d->m_metaData.isEmpty() && !options.testFlags(flags: KPluginMetaDataOption::AllowEmptyMetaData)) {
172 qCDebug(KCOREADDONS_DEBUG) << "plugin metadata in" << pluginFile << "does not have a valid 'MetaData' object";
173 }
174 if (const QString id = d->m_rootObj[QLatin1String("Id")].toString(); !id.isEmpty()) {
175 if (id != d->m_pluginId) {
176 qWarning(catFunc: KCOREADDONS_DEBUG) << "The plugin" << pluginFile
177 << "explicitly states an Id in the embedded metadata, which is different from the one derived from the filename"
178 << "The Id field from the KPlugin object in the metadata should be removed";
179 } else {
180 qInfo(catFunc: KCOREADDONS_DEBUG) << "The plugin" << pluginFile << "explicitly states an 'Id' in the embedded metadata."
181 << "This value should be removed, the resulting pluginId will not be affected by it";
182 }
183 }
184}
185
186KPluginMetaData::KPluginMetaData(const QPluginLoader &loader, KPluginMetaDataOptions options)
187 : d(new KPluginMetaDataPrivate(loader.metaData().value(key: QLatin1String("MetaData")).toObject(), loader.fileName(), options))
188{
189 if (!loader.fileName().isEmpty()) {
190 d->m_pluginId = QFileInfo(loader.fileName()).completeBaseName();
191 }
192}
193
194KPluginMetaData::KPluginMetaData(const QJsonObject &metaData, const QString &fileName)
195 : d(new KPluginMetaDataPrivate(metaData, fileName))
196{
197 auto nameFromMetaData = d->m_rootObj.constFind(QStringLiteral("Id"));
198 if (nameFromMetaData != d->m_rootObj.constEnd()) {
199 d->m_pluginId = nameFromMetaData.value().toString();
200 }
201 if (d->m_pluginId.isEmpty()) {
202 d->m_pluginId = QFileInfo(d->m_fileName).completeBaseName();
203 }
204}
205
206KPluginMetaData KPluginMetaData::findPluginById(const QString &directory, const QString &pluginId, KPluginMetaDataOptions options)
207{
208 QPluginLoader loader;
209 const QString fileName = directory + QLatin1Char('/') + pluginId;
210 KPluginMetaDataPrivate::pluginLoaderForPath(loader, path: fileName);
211 if (loader.load()) {
212 if (KPluginMetaData metaData(loader, options); metaData.isValid()) {
213 return metaData;
214 }
215 }
216
217 if (const auto staticOptional = KStaticPluginHelpers::findById(directory, pluginId)) {
218 KPluginMetaData data = KPluginMetaDataPrivate::ofStaticPlugin(pluginNamespace: directory, fileName: pluginId, options, plugin: staticOptional.value());
219 Q_ASSERT(data.fileName() == fileName);
220 return data;
221 }
222
223 return KPluginMetaData{};
224}
225
226KPluginMetaData KPluginMetaData::fromJsonFile(const QString &file)
227{
228 QFile f(file);
229 bool b = f.open(flags: QIODevice::ReadOnly);
230 if (!b) {
231 qCWarning(KCOREADDONS_DEBUG) << "Couldn't open" << file;
232 return {};
233 }
234 QJsonParseError error;
235 const QJsonObject metaData = QJsonDocument::fromJson(json: f.readAll(), error: &error).object();
236 if (error.error) {
237 qCWarning(KCOREADDONS_DEBUG) << "error parsing" << file << error.errorString();
238 }
239
240 return KPluginMetaData(metaData, QFileInfo(file).absoluteFilePath());
241}
242
243QJsonObject KPluginMetaData::rawData() const
244{
245 return d->m_metaData;
246}
247
248QString KPluginMetaData::fileName() const
249{
250 return d->m_fileName;
251}
252QList<KPluginMetaData>
253KPluginMetaData::findPlugins(const QString &directory, std::function<bool(const KPluginMetaData &)> filter, KPluginMetaDataOptions options)
254{
255 QList<KPluginMetaData> ret;
256 const auto staticPlugins = KStaticPluginHelpers::staticPlugins(directory);
257 for (auto it = staticPlugins.begin(); it != staticPlugins.end(); ++it) {
258 KPluginMetaData metaData = KPluginMetaDataPrivate::ofStaticPlugin(pluginNamespace: directory, fileName: it.key(), options, plugin: it.value());
259 if (metaData.isValid()) {
260 if (!filter || filter(metaData)) {
261 ret << metaData;
262 }
263 }
264 }
265 QSet<QString> addedPluginIds;
266 const qint64 nowTs = QDateTime::currentMSecsSinceEpoch(); // For the initial load, stating all files is not needed
267 const bool checkCache = options.testFlags(flags: KPluginMetaData::CacheMetaData);
268 std::vector<KPluginMetaData> &cache = (*s_pluginNamespaceCache)[directory];
269 KPluginMetaDataPrivate::forEachPlugin(directory, callback: [&](const QFileInfo &pluginInfo) {
270 const QString pluginFile = pluginInfo.absoluteFilePath();
271
272 KPluginMetaData metadata;
273 if (checkCache) {
274 const auto it = std::find_if(first: cache.begin(), last: cache.end(), pred: [&pluginFile](const KPluginMetaData &data) {
275 return pluginFile == data.fileName();
276 });
277 bool isNew = it == cache.cend();
278 if (!isNew) {
279 const qint64 lastQueried = (*it).d->m_lastQueriedTs;
280 Q_ASSERT(lastQueried > 0);
281 isNew = lastQueried < pluginInfo.lastModified().toMSecsSinceEpoch();
282 }
283 if (!isNew) {
284 metadata = *it;
285 } else {
286 metadata = KPluginMetaData(pluginFile, options);
287 metadata.d->m_lastQueriedTs = nowTs;
288 cache.push_back(x: metadata);
289 }
290 } else {
291 metadata = KPluginMetaData(pluginFile, options);
292 }
293 if (!metadata.isValid()) {
294 qCDebug(KCOREADDONS_DEBUG) << pluginFile << "does not contain valid JSON metadata";
295 return;
296 }
297 if (addedPluginIds.contains(value: metadata.pluginId())) {
298 return;
299 }
300 if (filter && !filter(metadata)) {
301 return;
302 }
303 addedPluginIds << metadata.pluginId();
304 ret.append(t: metadata);
305 });
306 return ret;
307}
308
309bool KPluginMetaData::isValid() const
310{
311 // it can be valid even if m_fileName is empty (as long as the plugin id is
312 // set)
313 return !pluginId().isEmpty() && (!d->m_metaData.isEmpty() || d->m_options.testFlags(flags: AllowEmptyMetaData));
314}
315
316bool KPluginMetaData::isHidden() const
317{
318 return d->m_rootObj[QLatin1String("Hidden")].toBool();
319}
320
321static inline void addPersonFromJson(const QJsonObject &obj, QList<KAboutPerson> *out)
322{
323 KAboutPerson person = KAboutPerson::fromJSON(obj);
324 if (person.name().isEmpty()) {
325 qCWarning(KCOREADDONS_DEBUG) << "Invalid plugin metadata: Attempting to create a KAboutPerson from JSON without 'Name' property:" << obj;
326 return;
327 }
328 out->append(t: person);
329}
330
331static QList<KAboutPerson> aboutPersonFromJSON(const QJsonValue &people)
332{
333 QList<KAboutPerson> ret;
334 if (people.isObject()) {
335 // single author
336 addPersonFromJson(obj: people.toObject(), out: &ret);
337 } else if (people.isArray()) {
338 const QJsonArray peopleArray = people.toArray();
339 for (const QJsonValue &val : peopleArray) {
340 if (val.isObject()) {
341 addPersonFromJson(obj: val.toObject(), out: &ret);
342 }
343 }
344 }
345 return ret;
346}
347
348QList<KAboutPerson> KPluginMetaData::authors() const
349{
350 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("Authors")]);
351}
352
353QList<KAboutPerson> KPluginMetaData::translators() const
354{
355 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("Translators")]);
356}
357
358QList<KAboutPerson> KPluginMetaData::otherContributors() const
359{
360 return aboutPersonFromJSON(people: d->m_rootObj[QLatin1String("OtherContributors")]);
361}
362
363QString KPluginMetaData::category() const
364{
365 return d->m_rootObj[QLatin1String("Category")].toString();
366}
367
368QString KPluginMetaData::description() const
369{
370 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Description"));
371}
372
373QString KPluginMetaData::iconName() const
374{
375 return d->m_rootObj[QLatin1String("Icon")].toString();
376}
377
378QString KPluginMetaData::license() const
379{
380 return d->m_rootObj[QLatin1String("License")].toString();
381}
382
383QString KPluginMetaData::licenseText() const
384{
385 return KAboutLicense::byKeyword(keyword: license()).text();
386}
387
388QString KPluginMetaData::name() const
389{
390 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Name"));
391}
392
393QString KPluginMetaData::copyrightText() const
394{
395 return KJsonUtils::readTranslatedString(jo: d->m_rootObj, QStringLiteral("Copyright"));
396}
397
398QString KPluginMetaData::pluginId() const
399{
400 return d->m_pluginId;
401}
402
403QString KPluginMetaData::version() const
404{
405 return d->m_rootObj[QLatin1String("Version")].toString();
406}
407
408QString KPluginMetaData::website() const
409{
410 return d->m_rootObj[QLatin1String("Website")].toString();
411}
412
413QString KPluginMetaData::bugReportUrl() const
414{
415 return d->m_rootObj[QLatin1String("BugReportUrl")].toString();
416}
417
418QStringList KPluginMetaData::mimeTypes() const
419{
420 return d->m_rootObj[QLatin1String("MimeTypes")].toVariant().toStringList();
421}
422
423bool KPluginMetaData::supportsMimeType(const QString &mimeType) const
424{
425 // Check for exact matches first. This can delay parsing the full MIME
426 // database until later and noticeably speed up application startup on
427 // slower systems.
428 const QStringList mimes = mimeTypes();
429 if (mimes.contains(str: mimeType)) {
430 return true;
431 }
432
433 // Now check for MIME type inheritance to find non-exact matches:
434 QMimeDatabase db;
435 const QMimeType mime = db.mimeTypeForName(nameOrAlias: mimeType);
436 if (!mime.isValid()) {
437 return false;
438 }
439
440 return std::any_of(first: mimes.begin(), last: mimes.end(), pred: [&](const QString &supportedMimeName) {
441 return mime.inherits(mimeTypeName: supportedMimeName);
442 });
443}
444
445QStringList KPluginMetaData::formFactors() const
446{
447 return d->m_rootObj.value(key: QLatin1String("FormFactors")).toVariant().toStringList();
448}
449
450bool KPluginMetaData::isEnabledByDefault() const
451{
452 const QLatin1String key("EnabledByDefault");
453 const QJsonValue val = d->m_rootObj[key];
454 if (val.isBool()) {
455 return val.toBool();
456 } else if (val.isString()) {
457 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be boolean, but it was a string";
458 return val.toString() == QLatin1String("true");
459 }
460 return false;
461}
462
463QString KPluginMetaData::value(QStringView key, const QString &defaultValue) const
464{
465 const QJsonValue value = d->m_metaData.value(key);
466 if (value.isString()) {
467 return value.toString(defaultValue);
468 } else if (value.isArray()) {
469 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is an array";
470 return value.toVariant().toStringList().join(sep: QChar::fromLatin1(c: ','));
471 } else if (value.isBool()) {
472 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "in" << d->m_fileName << "to be a single string, but it is a bool";
473 return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
474 }
475 return defaultValue;
476}
477
478QString KPluginMetaData::value(const QString &key, const QString &defaultValue) const
479{
480 return value(key: QStringView(key), defaultValue);
481}
482
483bool KPluginMetaData::value(QStringView key, bool defaultValue) const
484{
485 const QJsonValue value = d->m_metaData.value(key);
486 if (value.isBool()) {
487 return value.toBool();
488 } else if (value.isString()) {
489 return value.toString() == QLatin1String("true");
490 } else {
491 return defaultValue;
492 }
493}
494
495bool KPluginMetaData::value(const QString &key, bool defaultValue) const
496{
497 return value(key: QStringView(key), defaultValue);
498}
499
500int KPluginMetaData::value(QStringView key, int defaultValue) const
501{
502 const QJsonValue value = d->m_metaData.value(key);
503 if (value.isDouble()) {
504 return value.toInt();
505 } else if (value.isString()) {
506 const QString intString = value.toString();
507 bool ok;
508 int convertedIntValue = intString.toInt(ok: &ok);
509 if (ok) {
510 return convertedIntValue;
511 } else {
512 qCWarning(KCOREADDONS_DEBUG) << "Expected" << key << "to be an int, instead" << intString << "was specified in the JSON metadata" << d->m_fileName;
513 return defaultValue;
514 }
515 } else {
516 return defaultValue;
517 }
518}
519
520int KPluginMetaData::value(const QString &key, int defaultValue) const
521{
522 return value(key: QStringView(key), defaultValue);
523}
524
525QStringList KPluginMetaData::value(QStringView key, const QStringList &defaultValue) const
526{
527 const QJsonValue value = d->m_metaData.value(key);
528 if (value.isUndefined() || value.isNull()) {
529 return defaultValue;
530 } else if (value.isObject()) {
531 qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list, instead an object was specified in" << d->m_fileName;
532 return defaultValue;
533 } else if (value.isArray()) {
534 return value.toVariant().toStringList();
535 } else {
536 const QString asString = value.isString() ? value.toString() : value.toVariant().toString();
537 if (asString.isEmpty()) {
538 return defaultValue;
539 }
540 qCDebug(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list in" << d->m_fileName
541 << "Treating it as a list with a single entry:" << asString;
542 return QStringList(asString);
543 }
544}
545
546QStringList KPluginMetaData::value(const QString &key, const QStringList &defaultValue) const
547{
548 return value(key: QStringView(key), defaultValue);
549}
550
551bool KPluginMetaData::operator==(const KPluginMetaData &other) const
552{
553 return d->m_fileName == other.d->m_fileName && d->m_metaData == other.d->m_metaData;
554}
555
556bool KPluginMetaData::isStaticPlugin() const
557{
558 return d->staticPlugin.has_value();
559}
560
561QString KPluginMetaData::requestedFileName() const
562{
563 return d->m_requestedFileName;
564}
565
566QStaticPlugin KPluginMetaData::staticPlugin() const
567{
568 Q_ASSERT(d);
569 Q_ASSERT(d->staticPlugin.has_value());
570 return d->staticPlugin.value();
571}
572
573QDebug operator<<(QDebug debug, const KPluginMetaData &metaData)
574{
575 QDebugStateSaver saver(debug);
576 debug.nospace() << "KPluginMetaData(pluginId:" << metaData.pluginId() << ", fileName: " << metaData.fileName() << ')';
577 return debug;
578}
579
580#include "moc_kpluginmetadata.cpp"
581

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