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 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(QLatin1String("MetaData")).toObject(), //
136 options);
137 ret->m_requestedFileName = path;
138 return ret;
139 }
140};
141
143 : d(new KPluginMetaDataPrivate(QJsonObject(), QString()))
144{
145}
146
148 : d(other.d)
149{
150}
151
153{
154 d = other.d;
155 return *this;
156}
157
159
161 : d(KPluginMetaDataPrivate::ofPath(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(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(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(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
184 : d(new KPluginMetaDataPrivate(loader.metaData().value(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
204{
205 QPluginLoader loader;
206 const QString fileName = directory + QLatin1Char('/') + pluginId;
207 KPluginMetaDataPrivate::pluginLoaderForPath(loader, 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(directory, pluginId, options, staticOptional.value());
216 Q_ASSERT(data.fileName() == fileName);
217 return data;
218 }
219
220 return KPluginMetaData{};
221}
222
224{
225 QFile f(file);
226 bool b = f.open(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(f.readAll(), &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}
250KPluginMetaData::findPlugins(const QString &directory, std::function<bool(const KPluginMetaData &)> filter, KPluginMetaDataOptions options)
251{
253 const auto staticPlugins = KStaticPluginHelpers::staticPlugins(directory);
254 for (auto it = staticPlugins.begin(); it != staticPlugins.end(); ++it) {
255 KPluginMetaData metaData = KPluginMetaDataPrivate::ofStaticPlugin(directory, it.key(), options, 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(KPluginMetaData::CacheMetaData);
265 std::vector<KPluginMetaData> &cache = (*s_pluginNamespaceCache)[directory];
266 KPluginMetaDataPrivate::forEachPlugin(directory, [&](const QFileInfo &pluginInfo) {
267 const QString pluginFile = pluginInfo.absoluteFilePath();
268
269 KPluginMetaData metadata;
270 if (checkCache) {
271 const auto it = std::find_if(cache.begin(), cache.end(), [&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(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(metadata.pluginId())) {
295 return;
296 }
297 if (filter && !filter(metadata)) {
298 return;
299 }
300 addedPluginIds << metadata.pluginId();
301 ret.append(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(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{
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(person);
326}
327
328static QList<KAboutPerson> aboutPersonFromJSON(const QJsonValue &people)
329{
331 if (people.isObject()) {
332 // single author
333 addPersonFromJson(people.toObject(), &ret);
334 } else if (people.isArray()) {
335 const QJsonArray peopleArray = people.toArray();
336 for (const QJsonValue &val : peopleArray) {
337 if (val.isObject()) {
338 addPersonFromJson(val.toObject(), &ret);
339 }
340 }
341 }
342 return ret;
343}
344
345QList<KAboutPerson> KPluginMetaData::authors() const
346{
347 return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Authors")]);
348}
349
350QList<KAboutPerson> KPluginMetaData::translators() const
351{
352 return aboutPersonFromJSON(d->m_rootObj[QLatin1String("Translators")]);
353}
354
355QList<KAboutPerson> KPluginMetaData::otherContributors() const
356{
357 return aboutPersonFromJSON(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(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(license()).text();
383}
384
385QString KPluginMetaData::name() const
386{
387 return KJsonUtils::readTranslatedString(d->m_rootObj, QStringLiteral("Name"));
388}
389
390QString KPluginMetaData::copyrightText() const
391{
392 return KJsonUtils::readTranslatedString(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
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(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(mimeType);
433 if (!mime.isValid()) {
434 return false;
435 }
436
437 return std::any_of(mimes.begin(), mimes.end(), [&](const QString &supportedMimeName) {
438 return mime.inherits(supportedMimeName);
439 });
440}
441
442QStringList KPluginMetaData::formFactors() const
443{
444 return d->m_rootObj.value(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(QChar::fromLatin1(','));
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);
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
528{
529 return d->m_fileName == other.d->m_fileName && d->m_metaData == other.d->m_metaData;
530}
531
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"
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...
QString value(const QString &key, const QString &defaultValue=QString()) const
KPluginMetaData()
Creates an invalid KPluginMetaData instance.
~KPluginMetaData()
Destructor.
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
KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::Alarm::Ptr &)
QString path(const QString &relativePath)
Absolute libexec path resolved in relative relation to the current shared object.
Definition klibexec.h:48
QAction * replace(const QObject *recvr, const char *slot, QObject *parent)
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
QString completeBaseName() 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 Fri Jun 7 2024 12:01:00 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.