Purpose

alternativesmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7#include "alternativesmodel.h"
8
9#if HAVE_QTDBUS
10#include <QDBusConnection>
11#include <QDBusConnectionInterface>
12#endif
13
14#include <QDebug>
15#include <QDirIterator>
16#include <QIcon>
17#include <QJsonArray>
18#include <QMimeDatabase>
19#include <QMimeType>
20#include <QRegularExpression>
21#include <QStandardPaths>
22
23#include <KConfigGroup>
24#include <KJsonUtils>
25#include <KPluginMetaData>
26#include <KSharedConfig>
27
28#include "configuration.h"
29#include "helper.h"
30#include "purpose_external_process_debug.h"
31
32using namespace Purpose;
33
34static const QStringList s_defaultDisabledPlugins = {QStringLiteral("saveasplugin")};
35
36typedef bool (*matchFunction)(const QString &constraint, const QJsonValue &value);
37
38static bool defaultMatch(const QString &constraint, const QJsonValue &value)
39{
40 return value == QJsonValue(constraint);
41}
42
43static bool mimeTypeMatch(const QString &constraint, const QJsonValue &value)
44{
45 if (value.isArray()) {
46 const auto array = value.toArray();
47 for (const QJsonValue &val : array) {
48 if (mimeTypeMatch(constraint, val)) {
49 return true;
50 }
51 }
52 return false;
53 } else if (value.isObject()) {
54 for (const QJsonValue &val : value.toObject()) {
55 if (mimeTypeMatch(constraint, val)) {
56 return true;
57 }
58 }
59 return false;
60 } else if (constraint.contains(QLatin1Char('*'))) {
62 return re.match(value.toString()).hasMatch();
63 } else {
65 QMimeType mime = db.mimeTypeForName(value.toString());
66 return mime.inherits(constraint);
67 }
68}
69
70static bool dbusMatch(const QString &constraint, const QJsonValue &value)
71{
72 Q_UNUSED(value)
73#if HAVE_QTDBUS
75#else
76 Q_UNUSED(constraint)
77 return false;
78#endif
79}
80
81static bool executablePresent(const QString &constraint, const QJsonValue &value)
82{
83 Q_UNUSED(value)
84 return !QStandardPaths::findExecutable(constraint).isEmpty();
85}
86
87static bool desktopFilePresent(const QString &constraint, const QJsonValue &value)
88{
89 Q_UNUSED(value)
91}
92
93static QMap<QString, matchFunction> s_matchFunctions = {
94 {QStringLiteral("mimeType"), mimeTypeMatch},
95 {QStringLiteral("dbus"), dbusMatch},
96 {QStringLiteral("application"), desktopFilePresent},
97 {QStringLiteral("exec"), executablePresent},
98};
99
100class Purpose::AlternativesModelPrivate
101{
102public:
103 QList<KPluginMetaData> m_plugins;
104 QJsonObject m_inputData;
105 QString m_pluginType;
106 QStringList m_disabledPlugins = s_defaultDisabledPlugins;
107 QJsonObject m_pluginTypeData;
108 const QRegularExpression constraintRx{QStringLiteral("(\\w+):(.*)")};
109
110 bool isPluginAcceptable(const KPluginMetaData &meta, const QStringList &disabledPlugins) const
111 {
112 const QJsonObject obj = meta.rawData();
113 if (!obj.value(QLatin1String("X-Purpose-PluginTypes")).toArray().contains(m_pluginType)) {
114 // qCDebug(PURPOSE_EXTERNAL_PROCESS_LOG) << "discarding" << meta.name() << KPluginMetaData::readStringList(meta.rawData(),
115 // QStringLiteral("X-Purpose-PluginTypes"));
116 return false;
117 }
118
119 if (disabledPlugins.contains(meta.pluginId()) || m_disabledPlugins.contains(meta.pluginId())) {
120 // qCDebug(PURPOSE_EXTERNAL_PROCESS_LOG) << "disabled plugin" << meta.name() << meta.pluginId();
121 return false;
122 }
123
124 // All constraints must match
125 const QJsonArray constraints = obj.value(QLatin1String("X-Purpose-Constraints")).toArray();
126 for (const QJsonValue &constraint : constraints) {
127 if (!constraintMatches(meta, constraint)) {
128 return false;
129 }
130 }
131 return true;
132 }
133
134 bool constraintMatches(const KPluginMetaData &meta, const QJsonValue &constraint) const
135 {
136 // Treat an array as an OR
137 if (constraint.isArray()) {
138 const QJsonArray options = constraint.toArray();
139 for (const auto &option : options) {
140 if (constraintMatches(meta, option)) {
141 return true;
142 }
143 }
144 return false;
145 }
146 Q_ASSERT(constraintRx.isValid());
147 QRegularExpressionMatch match = constraintRx.match(constraint.toString());
148 if (!match.isValid() || !match.hasMatch()) {
149 qCWarning(PURPOSE_EXTERNAL_PROCESS_LOG) << "wrong constraint" << constraint.toString();
150 return false;
151 }
152 const QString propertyName = match.captured(1);
153 const QString constrainedValue = match.captured(2);
154
155 const auto it = m_inputData.constFind(propertyName);
156 if (it == m_inputData.end()) {
157 // The constraint doesn't pertain to this data type
158 return true;
159 }
160
161 const bool acceptable = s_matchFunctions.value(propertyName, defaultMatch)(constrainedValue, *it);
162 if (!acceptable) {
163 // qCDebug(PURPOSE_EXTERNAL_PROCESS_LOG) << "not accepted" << meta.name() << propertyName << constrainedValue << *it;
164 }
165 return acceptable;
166 }
167};
168
169AlternativesModel::AlternativesModel(QObject *parent)
170 : QAbstractListModel(parent)
171 , d_ptr(new AlternativesModelPrivate)
172{
173}
174
175AlternativesModel::~AlternativesModel()
176{
178 delete d;
179}
180
181QHash<int, QByteArray> AlternativesModel::roleNames() const
182{
184 roles.insert(IconNameRole, QByteArrayLiteral("iconName"));
185 roles.insert(PluginIdRole, QByteArrayLiteral("pluginId"));
186 roles.insert(ActionDisplayRole, QByteArrayLiteral("actionDisplay"));
187 return roles;
188}
189
190void AlternativesModel::setInputData(const QJsonObject &input)
191{
193 if (input == d->m_inputData) {
194 return;
195 }
196
197 d->m_inputData = input;
198 initializeModel();
199
200 Q_EMIT inputDataChanged();
201}
202
203void AlternativesModel::setPluginType(const QString &pluginType)
204{
206 if (pluginType == d->m_pluginType) {
207 return;
208 }
209
210 d->m_pluginTypeData = Purpose::readPluginType(pluginType);
211 d->m_pluginType = pluginType;
212 Q_ASSERT(d->m_pluginTypeData.isEmpty() == d->m_pluginType.isEmpty());
213
214 initializeModel();
215
216 Q_EMIT pluginTypeChanged();
217}
218
220{
221 Q_D(const AlternativesModel);
222 return d->m_disabledPlugins;
223}
224
225void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds)
226{
228 if (pluginIds == d->m_disabledPlugins) {
229 return;
230 }
231
232 d->m_disabledPlugins = pluginIds;
233
234 initializeModel();
235
236 Q_EMIT disabledPluginsChanged();
237}
238
240{
241 Q_D(const AlternativesModel);
242 return d->m_pluginType;
243}
244
246{
247 Q_D(const AlternativesModel);
248 return d->m_inputData;
249}
250
252{
254 const KPluginMetaData pluginData = d->m_plugins.at(row);
255 return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this);
256}
257
258int AlternativesModel::rowCount(const QModelIndex &parent) const
259{
260 Q_D(const AlternativesModel);
261 return parent.isValid() ? 0 : d->m_plugins.count();
262}
263
264QVariant AlternativesModel::data(const QModelIndex &index, int role) const
265{
266 Q_D(const AlternativesModel);
267 if (!index.isValid() || index.row() > d->m_plugins.count()) {
268 return QVariant();
269 }
270
271 KPluginMetaData data = d->m_plugins[index.row()];
272 switch (role) {
273 case Qt::DisplayRole:
274 return data.name();
275 case Qt::ToolTipRole:
276 return data.description();
277 case IconNameRole:
278 return data.iconName();
280 return QIcon::fromTheme(data.iconName());
281 case PluginIdRole:
282 return data.pluginId();
283 case ActionDisplayRole: {
284 const QJsonObject pluginData = data.rawData().value(QLatin1String("KPlugin")).toObject();
285 QString action = KJsonUtils::readTranslatedString(pluginData, QStringLiteral("X-Purpose-ActionDisplay"));
286 // We really don't want custom keys in the KPlugin object, but used to do this. No warnings due to different release cycles for now
287 if (action.isEmpty()) {
288 action = KJsonUtils::readTranslatedString(data.rawData(), QStringLiteral("X-Purpose-ActionDisplay"));
289 }
290 return action.isEmpty() ? data.name() : action;
291 }
292 }
293 return QVariant();
294}
295
296static QList<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter)
297{
299 QSet<QString> addedPlugins;
300 const QStringList dirs =
302 for (const QString &dir : dirs) {
304
305 while (dirIt.hasNext()) {
306 QDir dir(dirIt.next());
307 Q_ASSERT(dir.exists());
308 if (!dir.exists(QStringLiteral("metadata.json"))) {
309 continue;
310 }
311
312 const KPluginMetaData info = Purpose::createMetaData(dir.absoluteFilePath(QStringLiteral("metadata.json")));
313 if (!addedPlugins.contains(info.pluginId()) && filter(info)) {
314 addedPlugins << info.pluginId();
315 ret += info;
316 }
317 }
318 }
319
320 return ret;
321}
322
323void AlternativesModel::initializeModel()
324{
326 if (d->m_pluginType.isEmpty()) {
327 return;
328 }
329
330 const QJsonArray inbound = d->m_pluginTypeData.value(QLatin1String("X-Purpose-InboundArguments")).toArray();
331 for (const QJsonValue &arg : inbound) {
332 const auto key = arg.toString();
333 if (!d->m_inputData.contains(key)) {
334 qCWarning(PURPOSE_EXTERNAL_PROCESS_LOG).nospace()
335 << "Cannot initialize model for plugin type " << d->m_pluginType << " with data " << d->m_inputData << ": missing key " << key;
336 return;
337 }
338 }
339
340 const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc"));
341 const auto group = config->group(QStringLiteral("plugins"));
342 const QStringList disabledPlugins = group.readEntry("disabled", QStringList());
343 auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) {
344 return d->isPluginAcceptable(meta, disabledPlugins);
345 };
346
348 d->m_plugins.clear();
349 d->m_plugins << KPluginMetaData::findPlugins(QStringLiteral("kf6/purpose"), pluginAcceptable);
350 d->m_plugins += findScriptedPackages(pluginAcceptable);
352}
353
354#include "moc_alternativesmodel.cpp"
QString pluginId() const
QJsonObject rawData() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
QString iconName() const
QString name() const
QString description() const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
Interface for client applications to share data.
QJsonObject inputData
Specifies the information that will be given to the plugin once it's started.
QString pluginType
Specifies the type of the plugin we want to list.
QStringList disabledPlugins
Provides a list of plugin names to have filtered out.
Q_SCRIPTABLE Purpose::Configuration * configureJob(int row)
This shouldn't require to have the job actually running on the same process as the app.
This class will be in charge of figuring out the job configuration.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT QString dir(const QString &fileClass)
virtual QHash< int, QByteArray > roleNames() const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
QDBusConnectionInterface * interface() const const
QDBusConnection sessionBus()
QDBusReply< bool > isServiceRegistered(const QString &serviceName) const const
iterator insert(const Key &key, const T &value)
QIcon fromTheme(const QString &name)
const_iterator constFind(QLatin1StringView key) const const
iterator end()
QJsonValue value(QLatin1StringView key) const const
bool isArray() const const
bool isObject() const const
QJsonArray toArray() const const
QJsonObject toObject() const const
QString toString() const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
int row() const const
Q_EMITQ_EMIT
QObject * parent() const const
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, MatchType matchType, MatchOptions matchOptions) const const
bool isValid() const const
QString wildcardToRegularExpression(QStringView pattern, WildcardConversionOptions options)
bool contains(const QSet< T > &other) const const
QString findExecutable(const QString &executableName, const QStringList &paths)
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
DisplayRole
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:06 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.