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 | |
32 | using namespace Purpose; |
33 | |
34 | static const QStringList s_defaultDisabledPlugins = {QStringLiteral("saveasplugin" )}; |
35 | |
36 | typedef bool (*matchFunction)(const QString &constraint, const QJsonValue &value); |
37 | |
38 | static bool defaultMatch(const QString &constraint, const QJsonValue &value) |
39 | { |
40 | return value == QJsonValue(constraint); |
41 | } |
42 | |
43 | static 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, value: 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, value: val)) { |
56 | return true; |
57 | } |
58 | } |
59 | return false; |
60 | } else if (constraint.contains(c: QLatin1Char('*'))) { |
61 | const QRegularExpression re(QRegularExpression::wildcardToRegularExpression(str: constraint), QRegularExpression::CaseInsensitiveOption); |
62 | return re.match(subject: value.toString()).hasMatch(); |
63 | } else { |
64 | QMimeDatabase db; |
65 | QMimeType mime = db.mimeTypeForName(nameOrAlias: value.toString()); |
66 | return mime.inherits(mimeTypeName: constraint); |
67 | } |
68 | } |
69 | |
70 | static bool dbusMatch(const QString &constraint, const QJsonValue &value) |
71 | { |
72 | Q_UNUSED(value) |
73 | #if HAVE_QTDBUS |
74 | return QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName: constraint); |
75 | #else |
76 | Q_UNUSED(constraint) |
77 | return false; |
78 | #endif |
79 | } |
80 | |
81 | static bool executablePresent(const QString &constraint, const QJsonValue &value) |
82 | { |
83 | Q_UNUSED(value) |
84 | return !QStandardPaths::findExecutable(executableName: constraint).isEmpty(); |
85 | } |
86 | |
87 | static bool desktopFilePresent(const QString &constraint, const QJsonValue &value) |
88 | { |
89 | Q_UNUSED(value) |
90 | return !QStandardPaths::locate(type: QStandardPaths::ApplicationsLocation, fileName: constraint).isEmpty(); |
91 | } |
92 | |
93 | static QMap<QString, matchFunction> s_matchFunctions = { |
94 | {QStringLiteral("mimeType" ), mimeTypeMatch}, |
95 | {QStringLiteral("dbus" ), dbusMatch}, |
96 | {QStringLiteral("application" ), desktopFilePresent}, |
97 | {QStringLiteral("exec" ), executablePresent}, |
98 | }; |
99 | |
100 | class Purpose::AlternativesModelPrivate |
101 | { |
102 | public: |
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(key: QLatin1String("X-Purpose-PluginTypes" )).toArray().contains(element: 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(str: meta.pluginId()) || m_disabledPlugins.contains(str: 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(key: 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, constraint: option)) { |
141 | return true; |
142 | } |
143 | } |
144 | return false; |
145 | } |
146 | Q_ASSERT(constraintRx.isValid()); |
147 | QRegularExpressionMatch match = constraintRx.match(subject: 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(nth: 1); |
153 | const QString constrainedValue = match.captured(nth: 2); |
154 | |
155 | const auto it = m_inputData.constFind(key: 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(key: propertyName, defaultValue: 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 | |
169 | AlternativesModel::AlternativesModel(QObject *parent) |
170 | : QAbstractListModel(parent) |
171 | , d_ptr(new AlternativesModelPrivate) |
172 | { |
173 | } |
174 | |
175 | AlternativesModel::~AlternativesModel() |
176 | { |
177 | Q_D(AlternativesModel); |
178 | delete d; |
179 | } |
180 | |
181 | QHash<int, QByteArray> AlternativesModel::roleNames() const |
182 | { |
183 | QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); |
184 | roles.insert(key: IconNameRole, QByteArrayLiteral("iconName" )); |
185 | roles.insert(key: PluginIdRole, QByteArrayLiteral("pluginId" )); |
186 | roles.insert(key: ActionDisplayRole, QByteArrayLiteral("actionDisplay" )); |
187 | return roles; |
188 | } |
189 | |
190 | void AlternativesModel::setInputData(const QJsonObject &input) |
191 | { |
192 | Q_D(AlternativesModel); |
193 | if (input == d->m_inputData) { |
194 | return; |
195 | } |
196 | |
197 | d->m_inputData = input; |
198 | initializeModel(); |
199 | |
200 | Q_EMIT inputDataChanged(); |
201 | } |
202 | |
203 | void AlternativesModel::setPluginType(const QString &pluginType) |
204 | { |
205 | Q_D(AlternativesModel); |
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 | |
219 | QStringList AlternativesModel::disabledPlugins() const |
220 | { |
221 | Q_D(const AlternativesModel); |
222 | return d->m_disabledPlugins; |
223 | } |
224 | |
225 | void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds) |
226 | { |
227 | Q_D(AlternativesModel); |
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 | |
239 | QString AlternativesModel::pluginType() const |
240 | { |
241 | Q_D(const AlternativesModel); |
242 | return d->m_pluginType; |
243 | } |
244 | |
245 | QJsonObject AlternativesModel::inputData() const |
246 | { |
247 | Q_D(const AlternativesModel); |
248 | return d->m_inputData; |
249 | } |
250 | |
251 | Purpose::Configuration *AlternativesModel::configureJob(int row) |
252 | { |
253 | Q_D(AlternativesModel); |
254 | const KPluginMetaData pluginData = d->m_plugins.at(i: row); |
255 | return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this); |
256 | } |
257 | |
258 | int AlternativesModel::rowCount(const QModelIndex &parent) const |
259 | { |
260 | Q_D(const AlternativesModel); |
261 | return parent.isValid() ? 0 : d->m_plugins.count(); |
262 | } |
263 | |
264 | QVariant 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(); |
279 | case Qt::DecorationRole: |
280 | return QIcon::fromTheme(name: data.iconName()); |
281 | case PluginIdRole: |
282 | return data.pluginId(); |
283 | case ActionDisplayRole: { |
284 | const QJsonObject pluginData = data.rawData().value(key: QLatin1String("KPlugin" )).toObject(); |
285 | QString action = KJsonUtils::readTranslatedString(jo: 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(jo: data.rawData(), QStringLiteral("X-Purpose-ActionDisplay" )); |
289 | } |
290 | return action.isEmpty() ? data.name() : action; |
291 | } |
292 | } |
293 | return QVariant(); |
294 | } |
295 | |
296 | static QList<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter) |
297 | { |
298 | QList<KPluginMetaData> ret; |
299 | QSet<QString> addedPlugins; |
300 | const QStringList dirs = |
301 | QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kpackage/Purpose" ), options: QStandardPaths::LocateDirectory); |
302 | for (const QString &dir : dirs) { |
303 | QDirIterator dirIt(dir, QDir::Dirs | QDir::NoDotAndDotDot); |
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(file: dir.absoluteFilePath(QStringLiteral("metadata.json" ))); |
313 | if (!addedPlugins.contains(value: info.pluginId()) && filter(info)) { |
314 | addedPlugins << info.pluginId(); |
315 | ret += info; |
316 | } |
317 | } |
318 | } |
319 | |
320 | return ret; |
321 | } |
322 | |
323 | void AlternativesModel::initializeModel() |
324 | { |
325 | Q_D(AlternativesModel); |
326 | |
327 | const QList<KPluginMetaData> newPlugins = [&d] { |
328 | QList<KPluginMetaData> plugins; |
329 | |
330 | if (d->m_pluginType.isEmpty() || d->m_inputData.isEmpty()) { |
331 | return plugins; |
332 | } |
333 | |
334 | const QJsonArray inbound = d->m_pluginTypeData.value(key: QLatin1String("X-Purpose-InboundArguments" )).toArray(); |
335 | for (const QJsonValue &arg : inbound) { |
336 | const auto key = arg.toString(); |
337 | if (!d->m_inputData.contains(key)) { |
338 | qCWarning(PURPOSE_EXTERNAL_PROCESS_LOG).nospace() |
339 | << "Cannot initialize model for plugin type " << d->m_pluginType << " with data " << d->m_inputData << ": missing key " << key; |
340 | return plugins; |
341 | } |
342 | } |
343 | |
344 | const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc" )); |
345 | const auto group = config->group(QStringLiteral("plugins" )); |
346 | const QStringList disabledPlugins = group.readEntry(key: "disabled" , aDefault: QStringList()); |
347 | auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) { |
348 | return d->isPluginAcceptable(meta, disabledPlugins); |
349 | }; |
350 | |
351 | plugins << KPluginMetaData::findPlugins(QStringLiteral("kf6/purpose" ), filter: pluginAcceptable); |
352 | plugins += findScriptedPackages(filter: pluginAcceptable); |
353 | |
354 | return plugins; |
355 | }(); |
356 | |
357 | if (d->m_plugins != newPlugins) { |
358 | beginResetModel(); |
359 | d->m_plugins = newPlugins; |
360 | endResetModel(); |
361 | } |
362 | } |
363 | |
364 | #include "moc_alternativesmodel.cpp" |
365 | |