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"