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#include <QDBusConnection>
9#include <QDBusConnectionInterface>
10#include <QDebug>
11#include <QDirIterator>
12#include <QIcon>
13#include <QJsonArray>
14#include <QMimeDatabase>
15#include <QMimeType>
16#include <QRegularExpression>
17#include <QStandardPaths>
18
19#include <KConfigGroup>
20#include <KJsonUtils>
21#include <KPluginMetaData>
22#include <KSharedConfig>
23
24#include "configuration.h"
25#include "helper.h"
26#include "job.h"
27
28using namespace Purpose;
29
30static const QStringList s_defaultDisabledPlugins = {QStringLiteral("saveasplugin")};
31
32typedef bool (*matchFunction)(const QString &constraint, const QJsonValue &value);
33
34static bool defaultMatch(const QString &constraint, const QJsonValue &value)
35{
36 return value == QJsonValue(constraint);
37}
38
39static bool mimeTypeMatch(const QString &constraint, const QJsonValue &value)
40{
41 if (value.isArray()) {
42 const auto array = value.toArray();
43 for (const QJsonValue &val : array) {
44 if (mimeTypeMatch(constraint, value: val))
45 return true;
46 }
47 return false;
48 } else if (value.isObject()) {
49 for (const QJsonValue &val : value.toObject()) {
50 if (mimeTypeMatch(constraint, value: val))
51 return true;
52 }
53 return false;
54 } else if (constraint.contains(c: QLatin1Char('*'))) {
55 const QRegularExpression re(QRegularExpression::wildcardToRegularExpression(str: constraint), QRegularExpression::CaseInsensitiveOption);
56 return re.match(subject: value.toString()).hasMatch();
57 } else {
58 QMimeDatabase db;
59 QMimeType mime = db.mimeTypeForName(nameOrAlias: value.toString());
60 return mime.inherits(mimeTypeName: constraint);
61 }
62}
63
64static bool dbusMatch(const QString &constraint, const QJsonValue &value)
65{
66 Q_UNUSED(value)
67 return QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName: constraint);
68}
69
70static bool executablePresent(const QString &constraint, const QJsonValue &value)
71{
72 Q_UNUSED(value)
73 return !QStandardPaths::findExecutable(executableName: constraint).isEmpty();
74}
75
76static bool desktopFilePresent(const QString &constraint, const QJsonValue &value)
77{
78 Q_UNUSED(value)
79 return !QStandardPaths::locate(type: QStandardPaths::ApplicationsLocation, fileName: constraint).isEmpty();
80}
81
82static QMap<QString, matchFunction> s_matchFunctions = {
83 {QStringLiteral("mimeType"), mimeTypeMatch},
84 {QStringLiteral("dbus"), dbusMatch},
85 {QStringLiteral("application"), desktopFilePresent},
86 {QStringLiteral("exec"), executablePresent},
87};
88
89class Purpose::AlternativesModelPrivate
90{
91public:
92 QList<KPluginMetaData> m_plugins;
93 QJsonObject m_inputData;
94 QString m_pluginType;
95 QStringList m_disabledPlugins = s_defaultDisabledPlugins;
96 QJsonObject m_pluginTypeData;
97 const QRegularExpression constraintRx{QStringLiteral("(\\w+):(.*)")};
98
99 bool isPluginAcceptable(const KPluginMetaData &meta, const QStringList &disabledPlugins) const
100 {
101 const QJsonObject obj = meta.rawData();
102 if (!obj.value(key: QLatin1String("X-Purpose-PluginTypes")).toArray().contains(element: m_pluginType)) {
103 // qDebug() << "discarding" << meta.name() << KPluginMetaData::readStringList(meta.rawData(), QStringLiteral("X-Purpose-PluginTypes"));
104 return false;
105 }
106
107 if (disabledPlugins.contains(str: meta.pluginId()) || m_disabledPlugins.contains(str: meta.pluginId())) {
108 // qDebug() << "disabled plugin" << meta.name() << meta.pluginId();
109 return false;
110 }
111
112 // All constraints must match
113 const QJsonArray constraints = obj.value(key: QLatin1String("X-Purpose-Constraints")).toArray();
114 for (const QJsonValue &constraint : constraints) {
115 if (!constraintMatches(meta, constraint))
116 return false;
117 }
118 return true;
119 }
120
121 bool constraintMatches(const KPluginMetaData &meta, const QJsonValue &constraint) const
122 {
123 // Treat an array as an OR
124 if (constraint.isArray()) {
125 const QJsonArray options = constraint.toArray();
126 for (const auto &option : options) {
127 if (constraintMatches(meta, constraint: option)) {
128 return true;
129 }
130 }
131 return false;
132 }
133 Q_ASSERT(constraintRx.isValid());
134 QRegularExpressionMatch match = constraintRx.match(subject: constraint.toString());
135 if (!match.isValid() || !match.hasMatch()) {
136 qWarning() << "wrong constraint" << constraint.toString();
137 return false;
138 }
139 const QString propertyName = match.captured(nth: 1);
140 const QString constrainedValue = match.captured(nth: 2);
141 const bool acceptable = s_matchFunctions.value(key: propertyName, defaultValue: defaultMatch)(constrainedValue, m_inputData.value(key: propertyName));
142 if (!acceptable) {
143 // qDebug() << "not accepted" << meta.name() << propertyName << constrainedValue << m_inputData[propertyName];
144 }
145 return acceptable;
146 }
147};
148
149AlternativesModel::AlternativesModel(QObject *parent)
150 : QAbstractListModel(parent)
151 , d_ptr(new AlternativesModelPrivate)
152{
153}
154
155AlternativesModel::~AlternativesModel()
156{
157 Q_D(AlternativesModel);
158 delete d;
159}
160
161QHash<int, QByteArray> AlternativesModel::roleNames() const
162{
163 QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
164 roles.insert(key: IconNameRole, QByteArrayLiteral("iconName"));
165 roles.insert(key: PluginIdRole, QByteArrayLiteral("pluginId"));
166 roles.insert(key: ActionDisplayRole, QByteArrayLiteral("actionDisplay"));
167 return roles;
168}
169
170void AlternativesModel::setInputData(const QJsonObject &input)
171{
172 Q_D(AlternativesModel);
173 if (input == d->m_inputData)
174 return;
175
176 d->m_inputData = input;
177 initializeModel();
178
179 Q_EMIT inputDataChanged();
180}
181
182void AlternativesModel::setPluginType(const QString &pluginType)
183{
184 Q_D(AlternativesModel);
185 if (pluginType == d->m_pluginType)
186 return;
187
188 d->m_pluginTypeData = Purpose::readPluginType(pluginType);
189 d->m_pluginType = pluginType;
190 Q_ASSERT(d->m_pluginTypeData.isEmpty() == d->m_pluginType.isEmpty());
191
192 initializeModel();
193
194 Q_EMIT pluginTypeChanged();
195}
196
197QStringList AlternativesModel::disabledPlugins() const
198{
199 Q_D(const AlternativesModel);
200 return d->m_disabledPlugins;
201}
202
203void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds)
204{
205 Q_D(AlternativesModel);
206 if (pluginIds == d->m_disabledPlugins)
207 return;
208
209 d->m_disabledPlugins = pluginIds;
210
211 initializeModel();
212
213 Q_EMIT disabledPluginsChanged();
214}
215
216QString AlternativesModel::pluginType() const
217{
218 Q_D(const AlternativesModel);
219 return d->m_pluginType;
220}
221
222QJsonObject AlternativesModel::inputData() const
223{
224 Q_D(const AlternativesModel);
225 return d->m_inputData;
226}
227
228Purpose::Configuration *AlternativesModel::configureJob(int row)
229{
230 Q_D(AlternativesModel);
231 const KPluginMetaData pluginData = d->m_plugins.at(i: row);
232 return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this);
233}
234
235int AlternativesModel::rowCount(const QModelIndex &parent) const
236{
237 Q_D(const AlternativesModel);
238 return parent.isValid() ? 0 : d->m_plugins.count();
239}
240
241QVariant AlternativesModel::data(const QModelIndex &index, int role) const
242{
243 Q_D(const AlternativesModel);
244 if (!index.isValid() || index.row() > d->m_plugins.count())
245 return QVariant();
246
247 KPluginMetaData data = d->m_plugins[index.row()];
248 switch (role) {
249 case Qt::DisplayRole:
250 return data.name();
251 case Qt::ToolTip:
252 return data.description();
253 case IconNameRole:
254 return data.iconName();
255 case Qt::DecorationRole:
256 return QIcon::fromTheme(name: data.iconName());
257 case PluginIdRole:
258 return data.pluginId();
259 case ActionDisplayRole: {
260 const QJsonObject pluginData = data.rawData().value(key: QLatin1String("KPlugin")).toObject();
261 const QString action = KJsonUtils::readTranslatedString(jo: pluginData, QStringLiteral("X-Purpose-ActionDisplay"));
262 return action.isEmpty() ? data.name() : action;
263 }
264 }
265 return QVariant();
266}
267
268static QList<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter)
269{
270 QList<KPluginMetaData> ret;
271 QSet<QString> addedPlugins;
272 const QStringList dirs =
273 QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kpackage/Purpose"), options: QStandardPaths::LocateDirectory);
274 for (const QString &dir : dirs) {
275 QDirIterator dirIt(dir, QDir::Dirs | QDir::NoDotAndDotDot);
276
277 for (; dirIt.hasNext();) {
278 QDir dir(dirIt.next());
279 Q_ASSERT(dir.exists());
280 if (!dir.exists(QStringLiteral("metadata.json")))
281 continue;
282
283 const KPluginMetaData info = Purpose::createMetaData(file: dir.absoluteFilePath(QStringLiteral("metadata.json")));
284 if (!addedPlugins.contains(value: info.pluginId()) && filter(info)) {
285 addedPlugins << info.pluginId();
286 ret += info;
287 }
288 }
289 }
290
291 return ret;
292}
293
294void AlternativesModel::initializeModel()
295{
296 Q_D(AlternativesModel);
297 if (d->m_pluginType.isEmpty()) {
298 return;
299 }
300
301 const QJsonArray inbound = d->m_pluginTypeData.value(key: QLatin1String("X-Purpose-InboundArguments")).toArray();
302 for (const QJsonValue &arg : inbound) {
303 if (!d->m_inputData.contains(key: arg.toString())) {
304 qWarning().nospace() << "Cannot initialize model with data " << d->m_inputData << ". missing: " << arg;
305 return;
306 }
307 }
308
309 const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc"));
310 const auto group = config->group(QStringLiteral("plugins"));
311 const QStringList disabledPlugins = group.readEntry(key: "disabled", aDefault: QStringList());
312 auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) {
313 return d->isPluginAcceptable(meta, disabledPlugins);
314 };
315
316 beginResetModel();
317 d->m_plugins.clear();
318 d->m_plugins << KPluginMetaData::findPlugins(QStringLiteral("kf6/purpose"), filter: pluginAcceptable);
319 d->m_plugins += findScriptedPackages(filter: pluginAcceptable);
320 endResetModel();
321}
322
323#include "moc_alternativesmodel.cpp"
324

source code of purpose/src/alternativesmodel.cpp