File indexing completed on 2024-03-24 04:03:21
0001 /* 0002 SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com> 0003 0004 SPDX-License-Identifier: LGPL-2.1-or-later 0005 */ 0006 0007 #include "alternativesmodel.h" 0008 #include <QDBusConnection> 0009 #include <QDBusConnectionInterface> 0010 #include <QDebug> 0011 #include <QDirIterator> 0012 #include <QIcon> 0013 #include <QJsonArray> 0014 #include <QMimeDatabase> 0015 #include <QMimeType> 0016 #include <QRegularExpression> 0017 #include <QStandardPaths> 0018 0019 #include <KConfigGroup> 0020 #include <KJsonUtils> 0021 #include <KPluginMetaData> 0022 #include <KSharedConfig> 0023 0024 #include "configuration.h" 0025 #include "helper.h" 0026 #include "job.h" 0027 0028 using namespace Purpose; 0029 0030 static const QStringList s_defaultDisabledPlugins = {QStringLiteral("saveasplugin")}; 0031 0032 typedef bool (*matchFunction)(const QString &constraint, const QJsonValue &value); 0033 0034 static bool defaultMatch(const QString &constraint, const QJsonValue &value) 0035 { 0036 return value == QJsonValue(constraint); 0037 } 0038 0039 static bool mimeTypeMatch(const QString &constraint, const QJsonValue &value) 0040 { 0041 if (value.isArray()) { 0042 const auto array = value.toArray(); 0043 for (const QJsonValue &val : array) { 0044 if (mimeTypeMatch(constraint, val)) 0045 return true; 0046 } 0047 return false; 0048 } else if (value.isObject()) { 0049 for (const QJsonValue &val : value.toObject()) { 0050 if (mimeTypeMatch(constraint, val)) 0051 return true; 0052 } 0053 return false; 0054 } else if (constraint.contains(QLatin1Char('*'))) { 0055 const QRegularExpression re(QRegularExpression::wildcardToRegularExpression(constraint), QRegularExpression::CaseInsensitiveOption); 0056 return re.match(value.toString()).hasMatch(); 0057 } else { 0058 QMimeDatabase db; 0059 QMimeType mime = db.mimeTypeForName(value.toString()); 0060 return mime.inherits(constraint); 0061 } 0062 } 0063 0064 static bool dbusMatch(const QString &constraint, const QJsonValue &value) 0065 { 0066 Q_UNUSED(value) 0067 return QDBusConnection::sessionBus().interface()->isServiceRegistered(constraint); 0068 } 0069 0070 static bool executablePresent(const QString &constraint, const QJsonValue &value) 0071 { 0072 Q_UNUSED(value) 0073 return !QStandardPaths::findExecutable(constraint).isEmpty(); 0074 } 0075 0076 static bool desktopFilePresent(const QString &constraint, const QJsonValue &value) 0077 { 0078 Q_UNUSED(value) 0079 return !QStandardPaths::locate(QStandardPaths::ApplicationsLocation, constraint).isEmpty(); 0080 } 0081 0082 static QMap<QString, matchFunction> s_matchFunctions = { 0083 {QStringLiteral("mimeType"), mimeTypeMatch}, 0084 {QStringLiteral("dbus"), dbusMatch}, 0085 {QStringLiteral("application"), desktopFilePresent}, 0086 {QStringLiteral("exec"), executablePresent}, 0087 }; 0088 0089 class Purpose::AlternativesModelPrivate 0090 { 0091 public: 0092 QList<KPluginMetaData> m_plugins; 0093 QJsonObject m_inputData; 0094 QString m_pluginType; 0095 QStringList m_disabledPlugins = s_defaultDisabledPlugins; 0096 QJsonObject m_pluginTypeData; 0097 const QRegularExpression constraintRx{QStringLiteral("(\\w+):(.*)")}; 0098 0099 bool isPluginAcceptable(const KPluginMetaData &meta, const QStringList &disabledPlugins) const 0100 { 0101 const QJsonObject obj = meta.rawData(); 0102 if (!obj.value(QLatin1String("X-Purpose-PluginTypes")).toArray().contains(m_pluginType)) { 0103 // qDebug() << "discarding" << meta.name() << KPluginMetaData::readStringList(meta.rawData(), QStringLiteral("X-Purpose-PluginTypes")); 0104 return false; 0105 } 0106 0107 if (disabledPlugins.contains(meta.pluginId()) || m_disabledPlugins.contains(meta.pluginId())) { 0108 // qDebug() << "disabled plugin" << meta.name() << meta.pluginId(); 0109 return false; 0110 } 0111 0112 // All constraints must match 0113 const QJsonArray constraints = obj.value(QLatin1String("X-Purpose-Constraints")).toArray(); 0114 for (const QJsonValue &constraint : constraints) { 0115 if (!constraintMatches(meta, constraint)) 0116 return false; 0117 } 0118 return true; 0119 } 0120 0121 bool constraintMatches(const KPluginMetaData &meta, const QJsonValue &constraint) const 0122 { 0123 // Treat an array as an OR 0124 if (constraint.isArray()) { 0125 const QJsonArray options = constraint.toArray(); 0126 for (const auto &option : options) { 0127 if (constraintMatches(meta, option)) { 0128 return true; 0129 } 0130 } 0131 return false; 0132 } 0133 Q_ASSERT(constraintRx.isValid()); 0134 QRegularExpressionMatch match = constraintRx.match(constraint.toString()); 0135 if (!match.isValid() || !match.hasMatch()) { 0136 qWarning() << "wrong constraint" << constraint.toString(); 0137 return false; 0138 } 0139 const QString propertyName = match.captured(1); 0140 const QString constrainedValue = match.captured(2); 0141 const bool acceptable = s_matchFunctions.value(propertyName, defaultMatch)(constrainedValue, m_inputData.value(propertyName)); 0142 if (!acceptable) { 0143 // qDebug() << "not accepted" << meta.name() << propertyName << constrainedValue << m_inputData[propertyName]; 0144 } 0145 return acceptable; 0146 } 0147 }; 0148 0149 AlternativesModel::AlternativesModel(QObject *parent) 0150 : QAbstractListModel(parent) 0151 , d_ptr(new AlternativesModelPrivate) 0152 { 0153 } 0154 0155 AlternativesModel::~AlternativesModel() 0156 { 0157 Q_D(AlternativesModel); 0158 delete d; 0159 } 0160 0161 QHash<int, QByteArray> AlternativesModel::roleNames() const 0162 { 0163 QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); 0164 roles.insert(IconNameRole, QByteArrayLiteral("iconName")); 0165 roles.insert(PluginIdRole, QByteArrayLiteral("pluginId")); 0166 roles.insert(ActionDisplayRole, QByteArrayLiteral("actionDisplay")); 0167 return roles; 0168 } 0169 0170 void AlternativesModel::setInputData(const QJsonObject &input) 0171 { 0172 Q_D(AlternativesModel); 0173 if (input == d->m_inputData) 0174 return; 0175 0176 d->m_inputData = input; 0177 initializeModel(); 0178 0179 Q_EMIT inputDataChanged(); 0180 } 0181 0182 void AlternativesModel::setPluginType(const QString &pluginType) 0183 { 0184 Q_D(AlternativesModel); 0185 if (pluginType == d->m_pluginType) 0186 return; 0187 0188 d->m_pluginTypeData = Purpose::readPluginType(pluginType); 0189 d->m_pluginType = pluginType; 0190 Q_ASSERT(d->m_pluginTypeData.isEmpty() == d->m_pluginType.isEmpty()); 0191 0192 initializeModel(); 0193 0194 Q_EMIT pluginTypeChanged(); 0195 } 0196 0197 QStringList AlternativesModel::disabledPlugins() const 0198 { 0199 Q_D(const AlternativesModel); 0200 return d->m_disabledPlugins; 0201 } 0202 0203 void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds) 0204 { 0205 Q_D(AlternativesModel); 0206 if (pluginIds == d->m_disabledPlugins) 0207 return; 0208 0209 d->m_disabledPlugins = pluginIds; 0210 0211 initializeModel(); 0212 0213 Q_EMIT disabledPluginsChanged(); 0214 } 0215 0216 QString AlternativesModel::pluginType() const 0217 { 0218 Q_D(const AlternativesModel); 0219 return d->m_pluginType; 0220 } 0221 0222 QJsonObject AlternativesModel::inputData() const 0223 { 0224 Q_D(const AlternativesModel); 0225 return d->m_inputData; 0226 } 0227 0228 Purpose::Configuration *AlternativesModel::configureJob(int row) 0229 { 0230 Q_D(AlternativesModel); 0231 const KPluginMetaData pluginData = d->m_plugins.at(row); 0232 return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this); 0233 } 0234 0235 int AlternativesModel::rowCount(const QModelIndex &parent) const 0236 { 0237 Q_D(const AlternativesModel); 0238 return parent.isValid() ? 0 : d->m_plugins.count(); 0239 } 0240 0241 QVariant AlternativesModel::data(const QModelIndex &index, int role) const 0242 { 0243 Q_D(const AlternativesModel); 0244 if (!index.isValid() || index.row() > d->m_plugins.count()) 0245 return QVariant(); 0246 0247 KPluginMetaData data = d->m_plugins[index.row()]; 0248 switch (role) { 0249 case Qt::DisplayRole: 0250 return data.name(); 0251 case Qt::ToolTip: 0252 return data.description(); 0253 case IconNameRole: 0254 return data.iconName(); 0255 case Qt::DecorationRole: 0256 return QIcon::fromTheme(data.iconName()); 0257 case PluginIdRole: 0258 return data.pluginId(); 0259 case ActionDisplayRole: { 0260 const QJsonObject pluginData = data.rawData().value(QLatin1String("KPlugin")).toObject(); 0261 const QString action = KJsonUtils::readTranslatedString(pluginData, QStringLiteral("X-Purpose-ActionDisplay")); 0262 return action.isEmpty() ? data.name() : action; 0263 } 0264 } 0265 return QVariant(); 0266 } 0267 0268 static QList<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter) 0269 { 0270 QList<KPluginMetaData> ret; 0271 QSet<QString> addedPlugins; 0272 const QStringList dirs = 0273 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kpackage/Purpose"), QStandardPaths::LocateDirectory); 0274 for (const QString &dir : dirs) { 0275 QDirIterator dirIt(dir, QDir::Dirs | QDir::NoDotAndDotDot); 0276 0277 for (; dirIt.hasNext();) { 0278 QDir dir(dirIt.next()); 0279 Q_ASSERT(dir.exists()); 0280 if (!dir.exists(QStringLiteral("metadata.json"))) 0281 continue; 0282 0283 const KPluginMetaData info = Purpose::createMetaData(dir.absoluteFilePath(QStringLiteral("metadata.json"))); 0284 if (!addedPlugins.contains(info.pluginId()) && filter(info)) { 0285 addedPlugins << info.pluginId(); 0286 ret += info; 0287 } 0288 } 0289 } 0290 0291 return ret; 0292 } 0293 0294 void AlternativesModel::initializeModel() 0295 { 0296 Q_D(AlternativesModel); 0297 if (d->m_pluginType.isEmpty()) { 0298 return; 0299 } 0300 0301 const QJsonArray inbound = d->m_pluginTypeData.value(QLatin1String("X-Purpose-InboundArguments")).toArray(); 0302 for (const QJsonValue &arg : inbound) { 0303 if (!d->m_inputData.contains(arg.toString())) { 0304 qWarning().nospace() << "Cannot initialize model with data " << d->m_inputData << ". missing: " << arg; 0305 return; 0306 } 0307 } 0308 0309 const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc")); 0310 const auto group = config->group(QStringLiteral("plugins")); 0311 const QStringList disabledPlugins = group.readEntry("disabled", QStringList()); 0312 auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) { 0313 return d->isPluginAcceptable(meta, disabledPlugins); 0314 }; 0315 0316 beginResetModel(); 0317 d->m_plugins.clear(); 0318 d->m_plugins << KPluginMetaData::findPlugins(QStringLiteral("kf6/purpose"), pluginAcceptable); 0319 d->m_plugins += findScriptedPackages(pluginAcceptable); 0320 endResetModel(); 0321 } 0322 0323 #include "moc_alternativesmodel.cpp"