File indexing completed on 2024-04-28 15:33:58
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 = {{QStringLiteral("mimeType"), mimeTypeMatch}, 0083 {QStringLiteral("dbus"), dbusMatch}, 0084 {QStringLiteral("application"), desktopFilePresent}, 0085 {QStringLiteral("exec"), executablePresent}}; 0086 0087 class Purpose::AlternativesModelPrivate 0088 { 0089 public: 0090 QVector<KPluginMetaData> m_plugins; 0091 QJsonObject m_inputData; 0092 QString m_pluginType; 0093 QStringList m_disabledPlugins = s_defaultDisabledPlugins; 0094 QJsonObject m_pluginTypeData; 0095 const QRegularExpression constraintRx{QStringLiteral("(\\w+):(.*)")}; 0096 0097 bool isPluginAcceptable(const KPluginMetaData &meta, const QStringList &disabledPlugins) const 0098 { 0099 const QJsonObject obj = meta.rawData(); 0100 if (!obj.value(QStringLiteral("X-Purpose-PluginTypes")).toArray().contains(m_pluginType)) { 0101 // qDebug() << "discarding" << meta.name() << KPluginMetaData::readStringList(meta.rawData(), QStringLiteral("X-Purpose-PluginTypes")); 0102 return false; 0103 } 0104 0105 if (disabledPlugins.contains(meta.pluginId()) || m_disabledPlugins.contains(meta.pluginId())) { 0106 // qDebug() << "disabled plugin" << meta.name() << meta.pluginId(); 0107 return false; 0108 } 0109 0110 // All constraints must match 0111 const QJsonArray constraints = obj.value(QStringLiteral("X-Purpose-Constraints")).toArray(); 0112 for (const QJsonValue &constraint : constraints) { 0113 if (!constraintMatches(meta, constraint)) 0114 return false; 0115 } 0116 return true; 0117 } 0118 0119 bool constraintMatches(const KPluginMetaData &meta, const QJsonValue &constraint) const 0120 { 0121 // Treat an array as an OR 0122 if (constraint.isArray()) { 0123 const QJsonArray options = constraint.toArray(); 0124 for (const auto &option : options) { 0125 if (constraintMatches(meta, option)) { 0126 return true; 0127 } 0128 } 0129 return false; 0130 } 0131 Q_ASSERT(constraintRx.isValid()); 0132 QRegularExpressionMatch match = constraintRx.match(constraint.toString()); 0133 if (!match.isValid() || !match.hasMatch()) { 0134 qWarning() << "wrong constraint" << constraint.toString(); 0135 return false; 0136 } 0137 const QString propertyName = match.captured(1); 0138 const QString constrainedValue = match.captured(2); 0139 const bool acceptable = s_matchFunctions.value(propertyName, defaultMatch)(constrainedValue, m_inputData.value(propertyName)); 0140 if (!acceptable) { 0141 // qDebug() << "not accepted" << meta.name() << propertyName << constrainedValue << m_inputData[propertyName]; 0142 } 0143 return acceptable; 0144 } 0145 }; 0146 0147 AlternativesModel::AlternativesModel(QObject *parent) 0148 : QAbstractListModel(parent) 0149 , d_ptr(new AlternativesModelPrivate) 0150 { 0151 } 0152 0153 AlternativesModel::~AlternativesModel() 0154 { 0155 Q_D(AlternativesModel); 0156 delete d; 0157 } 0158 0159 QHash<int, QByteArray> AlternativesModel::roleNames() const 0160 { 0161 QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); 0162 roles.insert(IconNameRole, QByteArrayLiteral("iconName")); 0163 roles.insert(PluginIdRole, QByteArrayLiteral("pluginId")); 0164 roles.insert(ActionDisplayRole, QByteArrayLiteral("actionDisplay")); 0165 return roles; 0166 } 0167 0168 void AlternativesModel::setInputData(const QJsonObject &input) 0169 { 0170 Q_D(AlternativesModel); 0171 if (input == d->m_inputData) 0172 return; 0173 0174 d->m_inputData = input; 0175 initializeModel(); 0176 0177 Q_EMIT inputDataChanged(); 0178 } 0179 0180 void AlternativesModel::setPluginType(const QString &pluginType) 0181 { 0182 Q_D(AlternativesModel); 0183 if (pluginType == d->m_pluginType) 0184 return; 0185 0186 d->m_pluginTypeData = Purpose::readPluginType(pluginType); 0187 d->m_pluginType = pluginType; 0188 Q_ASSERT(d->m_pluginTypeData.isEmpty() == d->m_pluginType.isEmpty()); 0189 0190 initializeModel(); 0191 0192 Q_EMIT pluginTypeChanged(); 0193 } 0194 0195 QStringList AlternativesModel::disabledPlugins() const 0196 { 0197 Q_D(const AlternativesModel); 0198 return d->m_disabledPlugins; 0199 } 0200 0201 void AlternativesModel::setDisabledPlugins(const QStringList &pluginIds) 0202 { 0203 Q_D(AlternativesModel); 0204 if (pluginIds == d->m_disabledPlugins) 0205 return; 0206 0207 d->m_disabledPlugins = pluginIds; 0208 0209 initializeModel(); 0210 0211 Q_EMIT disabledPluginsChanged(); 0212 } 0213 0214 QString AlternativesModel::pluginType() const 0215 { 0216 Q_D(const AlternativesModel); 0217 return d->m_pluginType; 0218 } 0219 0220 QJsonObject AlternativesModel::inputData() const 0221 { 0222 Q_D(const AlternativesModel); 0223 return d->m_inputData; 0224 } 0225 0226 Purpose::Configuration *AlternativesModel::configureJob(int row) 0227 { 0228 Q_D(AlternativesModel); 0229 const KPluginMetaData pluginData = d->m_plugins.at(row); 0230 return new Configuration(d->m_inputData, d->m_pluginType, d->m_pluginTypeData, pluginData, this); 0231 } 0232 0233 int AlternativesModel::rowCount(const QModelIndex &parent) const 0234 { 0235 Q_D(const AlternativesModel); 0236 return parent.isValid() ? 0 : d->m_plugins.count(); 0237 } 0238 0239 QVariant AlternativesModel::data(const QModelIndex &index, int role) const 0240 { 0241 Q_D(const AlternativesModel); 0242 if (!index.isValid() || index.row() > d->m_plugins.count()) 0243 return QVariant(); 0244 0245 KPluginMetaData data = d->m_plugins[index.row()]; 0246 switch (role) { 0247 case Qt::DisplayRole: 0248 return data.name(); 0249 case Qt::ToolTip: 0250 return data.description(); 0251 case IconNameRole: 0252 return data.iconName(); 0253 case Qt::DecorationRole: 0254 return QIcon::fromTheme(data.iconName()); 0255 case PluginIdRole: 0256 return data.pluginId(); 0257 case ActionDisplayRole: { 0258 const auto pluginData = data.rawData()[QStringLiteral("KPlugin")].toObject(); 0259 const QString action = KJsonUtils::readTranslatedString(pluginData, QStringLiteral("X-Purpose-ActionDisplay")); 0260 return action.isEmpty() ? data.name() : action; 0261 } 0262 } 0263 return QVariant(); 0264 } 0265 0266 static QVector<KPluginMetaData> findScriptedPackages(std::function<bool(const KPluginMetaData &)> filter) 0267 { 0268 QVector<KPluginMetaData> ret; 0269 QSet<QString> addedPlugins; 0270 const QStringList dirs = 0271 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kpackage/Purpose"), QStandardPaths::LocateDirectory); 0272 for (const QString &dir : dirs) { 0273 QDirIterator dirIt(dir, QDir::Dirs | QDir::NoDotAndDotDot); 0274 0275 for (; dirIt.hasNext();) { 0276 QDir dir(dirIt.next()); 0277 Q_ASSERT(dir.exists()); 0278 if (!dir.exists(QStringLiteral("metadata.json"))) 0279 continue; 0280 0281 const KPluginMetaData info = Purpose::createMetaData(dir.absoluteFilePath(QStringLiteral("metadata.json"))); 0282 if (!addedPlugins.contains(info.pluginId()) && filter(info)) { 0283 addedPlugins << info.pluginId(); 0284 ret += info; 0285 } 0286 } 0287 } 0288 0289 return ret; 0290 } 0291 0292 void AlternativesModel::initializeModel() 0293 { 0294 Q_D(AlternativesModel); 0295 if (d->m_pluginType.isEmpty()) { 0296 return; 0297 } 0298 0299 const QJsonArray inbound = d->m_pluginTypeData.value(QStringLiteral("X-Purpose-InboundArguments")).toArray(); 0300 for (const QJsonValue &arg : inbound) { 0301 if (!d->m_inputData.contains(arg.toString())) { 0302 qWarning().nospace() << "Cannot initialize model with data " << d->m_inputData << ". missing: " << arg; 0303 return; 0304 } 0305 } 0306 0307 const auto config = KSharedConfig::openConfig(QStringLiteral("purposerc")); 0308 const auto group = config->group("plugins"); 0309 const QStringList disabledPlugins = group.readEntry("disabled", QStringList()); 0310 auto pluginAcceptable = [d, disabledPlugins](const KPluginMetaData &meta) { 0311 return d->isPluginAcceptable(meta, disabledPlugins); 0312 }; 0313 0314 beginResetModel(); 0315 d->m_plugins.clear(); 0316 d->m_plugins << KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/purpose"), pluginAcceptable); 0317 d->m_plugins += findScriptedPackages(pluginAcceptable); 0318 endResetModel(); 0319 } 0320 0321 #include "moc_alternativesmodel.cpp"