KCoreAddons

kpluginmetadata.cpp
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(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;
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(directory)) {
63 dirsToCheck << directory;
64 } else {
65 dirsToCheck = QCoreApplication::libraryPaths();
67 dirsToCheck.removeOne(appDirPath);
68 dirsToCheck.prepend(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(dirsToCheck)) {
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(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(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(QLatin1Char('/'))) { // Absolute path, use as it is
118 loader.setFileName(path);
119 } else {
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(QLatin1String("MetaData")).toObject(), //
139 options);
140 ret->m_requestedFileName = path;
141 return ret;
142 }
143};
144
146 : d(new KPluginMetaDataPrivate(QJsonObject(), QString()))
147{
148}
149
151 : d(other.d)
152{
153}
154
156{
157 d = other.d;
158 return *this;
159}
160
162
164 : d(KPluginMetaDataPrivate::ofPath(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(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(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(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
187 : d(new KPluginMetaDataPrivate(loader.metaData().value(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
207{
208 QPluginLoader loader;
209 const QString fileName = directory + QLatin1Char('/') + pluginId;
210 KPluginMetaDataPrivate::pluginLoaderForPath(loader, 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(directory, pluginId, options, staticOptional.value());
219 Q_ASSERT(data.fileName() == fileName);
220 return data;
221 }
222
223 return KPluginMetaData{};
224}
225
227{
228 QFile f(file);
229 bool b = f.open(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(f.readAll(), &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}
253KPluginMetaData::findPlugins(const QString &directory, std::function<bool(const KPluginMetaData &)> filter, KPluginMetaDataOptions options)
254{
256 const auto staticPlugins = KStaticPluginHelpers::staticPlugins(directory);
257 for (auto it = staticPlugins.begin(); it != staticPlugins.end(); ++it) {
258 KPluginMetaData metaData = KPluginMetaDataPrivate::ofStaticPlugin(directory, it.key(), options, 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(KPluginMetaData::CacheMetaData);
268 std::vector<KPluginMetaData> &cache = (*s_pluginNamespaceCache)[directory];
269 KPluginMetaDataPrivate::forEachPlugin(directory, [&](const QFileInfo &pluginInfo) {
270 const QString pluginFile = pluginInfo.absoluteFilePath();
271
272 KPluginMetaData metadata;
273 if (checkCache) {
274 const auto it = std::find_if(cache.begin(), cache.end(), [&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(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(metadata.pluginId())) {
298 return;
299 }
300 if (filter && !filter(metadata)) {
301 return;
302 }
303 addedPluginIds << metadata.pluginId();
304 ret.append(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(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{
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(person);
329}
330
331static QList<KAboutPerson> aboutPersonFromJSON(const QJsonValue &people)
332{
334 if (people.isObject()) {
335 // single author
336 addPersonFromJson(people.toObject(), &ret);
337 } else if (people.isArray()) {
338 const QJsonArray peopleArray = people.toArray();
339 for (const QJsonValue &val : peopleArray) {
340 if (val.isObject()) {
341 addPersonFromJson(val.toObject(), &ret);
342 }
343 }
344 }
345 return ret;
346}
347
348QList<KAboutPerson> KPluginMetaData::authors() const
349{
350 return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Authors")]);
351}
352
353QList<KAboutPerson> KPluginMetaData::translators() const
354{
355 return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Translators")]);
356}
357
358QList<KAboutPerson> KPluginMetaData::otherContributors() const
359{
360 return aboutPersonFromJSON(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(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(license()).text();
386}
387
388QString KPluginMetaData::name() const
389{
390 return KJsonUtils::readTranslatedString(d->m_rootObj, QStringLiteral("Name"));
391}
392
393QString KPluginMetaData::copyrightText() const
394{
395 return KJsonUtils::readTranslatedString(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
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(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(mimeType);
436 if (!mime.isValid()) {
437 return false;
438 }
439
440 return std::any_of(mimes.begin(), mimes.end(), [&](const QString &supportedMimeName) {
441 return mime.inherits(supportedMimeName);
442 });
443}
444
445QStringList KPluginMetaData::formFactors() const
446{
447 return d->m_rootObj.value(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(QChar::fromLatin1(','));
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(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(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);
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(QStringView(key), defaultValue);
523}
524
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(QStringView(key), defaultValue);
549}
550
552{
553 return d->m_fileName == other.d->m_fileName && d->m_metaData == other.d->m_metaData;
554}
555
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"
static KAboutLicense byKeyword(const QString &keyword)
Fetch a known license by a keyword/spdx ID.
This class is used to store information about a person or developer.
Definition kaboutdata.h:64
static KAboutPerson fromJSON(const QJsonObject &obj)
Creates a KAboutPerson from a JSON object with the following structure:
This class allows easily accessing some standardized values from the JSON metadata that can be embedd...
KPluginMetaData()
Creates an invalid KPluginMetaData instance.
~KPluginMetaData()
Destructor.
QString value(QStringView key, const QString &defaultValue=QString()) const
KPluginMetaData & operator=(const KPluginMetaData &)
Copy assignment.
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
Find all plugins inside directory.
bool operator==(const KPluginMetaData &other) const
static KPluginMetaData fromJsonFile(const QString &jsonFile)
Load a KPluginMetaData instance from a .json file.
static KPluginMetaData findPluginById(const QString &directory, const QString &pluginId, KPluginMetaDataOptions options={})
bool supportsMimeType(const QString &mimeType) const
@ AllowEmptyMetaData
Plugins with empty metaData are considered valid.
@ CacheMetaData
If KCoreAddons should keep metadata in cache.
bool isStaticPlugin() const
QString path(const QString &relativePath)
Absolute libexec path resolved in relative relation to the current shared object.
Definition klibexec.h:48
const QList< QKeySequence > & replace()
QDebug operator<<(QDebug dbg, const PerceptualColor::MultiSpinBoxSection &value)
QChar fromLatin1(char c)
QString applicationDirPath()
QStringList libraryPaths()
qint64 currentMSecsSinceEpoch()
qint64 toMSecsSinceEpoch() const const
QDebug & nospace()
bool isAbsolutePath(const QString &path)
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
QString absoluteFilePath() const const
QDateTime lastModified() const const
bool testFlags(QFlags< T > flags) const const
QByteArray readAll()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
bool isEmpty() const const
QJsonValue value(QLatin1StringView key) const const
bool isArray() const const
bool isBool() const const
bool isObject() const const
bool isString() const const
QJsonArray toArray() const const
bool toBool(bool defaultValue) const const
QJsonObject toObject() const const
QString toString() const const
bool isLibrary(const QString &fileName)
void append(QList< T > &&value)
iterator begin()
iterator end()
void prepend(parameter_type value)
bool removeOne(const AT &t)
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool isValid() const const
QString errorString() const const
void setFileName(const QString &fileName)
QJsonObject metaData() const const
bool contains(const QSet< T > &other) const const
QJsonObject metaData() const const
bool isEmpty() const const
bool isNull() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:08:22 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.