File indexing completed on 2024-12-15 04:01:20

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "plugin.hpp"
0008 
0009 #include <QJsonDocument>
0010 #include <QJsonObject>
0011 #include <QJsonArray>
0012 
0013 #include "app/scripting/script_engine.hpp"
0014 #include "app/application.hpp"
0015 
0016 #include "plugin/action.hpp"
0017 #include "plugin/io.hpp"
0018 #include "plugin/executor.hpp"
0019 
0020 using namespace glaxnimate;
0021 
0022 bool plugin::Plugin::run_script ( const plugin::PluginScript& script, const QVariantList& args ) const
0023 {
0024     if ( !data_.engine )
0025     {
0026         logger().log("Can't run script from a plugin with no engine", app::log::Error);
0027         return false;
0028     }
0029 
0030     if ( !PluginRegistry::instance().executor() )
0031     {
0032         logger().log("No script executor", app::log::Error);
0033         return false;
0034     }
0035 
0036     return PluginRegistry::instance().executor()->execute(*this, script, args);
0037 }
0038 
0039 void plugin::PluginRegistry::load()
0040 {
0041     QString writable_path = app::Application::instance()->writable_data_path("plugins");
0042 
0043     for ( const QString& path : app::Application::instance()->data_paths("plugins") )
0044     {
0045         bool writable = path == writable_path;
0046         QDir pathdir(path);
0047         for ( const auto& entry : pathdir.entryList(QDir::Dirs|QDir::NoDotAndDotDot|QDir::Readable) )
0048         {
0049             QDir entrydir(pathdir.absoluteFilePath(entry));
0050             if ( entrydir.exists("plugin.json") )
0051             {
0052                 load_plugin(entrydir.absoluteFilePath("plugin.json"), writable);
0053             }
0054         }
0055     }
0056     Q_EMIT loaded();
0057 }
0058 
0059 bool plugin::PluginRegistry::load_plugin ( const QString& path, bool user_installed )
0060 {
0061     logger.set_detail(path);
0062     QFileInfo file_info(path);
0063     if ( !file_info.exists() || !file_info.isFile() || !file_info.isReadable() )
0064     {
0065         logger.stream() << "Cannot read plugin file";
0066         return false;
0067     }
0068 
0069     QFile file(file_info.absoluteFilePath());
0070     if ( !file.open(QFile::ReadOnly) )
0071     {
0072         logger.stream() << "Cannot read plugin file";
0073         return false;
0074     }
0075 
0076     QJsonDocument jdoc;
0077 
0078     try {
0079         jdoc = QJsonDocument::fromJson(file.readAll());
0080     } catch ( const QJsonParseError& err ) {
0081         logger.stream() << "Invalid plugin file:" << err.errorString();
0082         return false;
0083     }
0084 
0085     if ( !jdoc.isObject() )
0086     {
0087         logger.stream() << "Invalid plugin file: not an object";
0088         return false;
0089     }
0090 
0091     const QJsonObject jobj = jdoc.object();
0092 
0093     PluginData data;
0094     data.dir = file_info.dir();
0095     data.id = QFileInfo(data.dir.path()).fileName();
0096     data.version = jobj["version"].toInt(0);
0097 
0098     auto it = names.find(data.id);
0099     int overwrite = -1;
0100     if ( it != names.end() )
0101     {
0102         Plugin* plug = plugins_[*it].get();
0103         if ( plug->data().version >= data.version )
0104         {
0105             logger.stream(app::log::Info) << "Skipping Plugin (newer version exists)";
0106             return false;
0107         }
0108 
0109         if ( plug->enabled() )
0110         {
0111             logger.stream(app::log::Info) << "Skipping Plugin (older version is currently enabled)";
0112             return false;
0113         }
0114 
0115         overwrite = *it;
0116     }
0117 
0118     data.engine_name = jobj["engine"].toString();
0119     data.engine = app::scripting::ScriptEngineFactory::instance().engine(data.engine_name);
0120     if ( !data.engine)
0121     {
0122         logger.stream() << "Plugin refers to an unknown engine" << data.engine_name;
0123     }
0124 
0125 
0126     data.name = jobj["name"].toString(data.id);
0127     data.author = jobj["author"].toString();
0128     data.icon = jobj["icon"].toString();
0129     data.description = jobj["description"].toString();
0130 
0131     QJsonArray arr = jobj["services"].toArray();
0132     if ( arr.empty() )
0133     {
0134         logger.stream() << "Plugin does not provide any services";
0135         return false;
0136     }
0137 
0138     for ( QJsonValue val : arr )
0139     {
0140         if ( !val.isObject() )
0141             logger.stream() << "Skipping invalid service";
0142         else
0143             load_service(val.toObject(), data);
0144     }
0145 
0146     if ( data.services.empty() )
0147     {
0148         logger.stream() << "Plugin does not provide any valid services";
0149         return false;
0150     }
0151 
0152     std::unique_ptr<Plugin> plugin = std::make_unique<Plugin>(std::move(data), user_installed);
0153 
0154     if ( overwrite != -1 )
0155     {
0156         plugins_[overwrite] = std::move(plugin);
0157     }
0158     else
0159     {
0160         names[plugin->data().id] = plugins_.size();
0161         plugins_.push_back(std::move(plugin));
0162     }
0163     return true;
0164 }
0165 
0166 void plugin::PluginRegistry::load_service ( const QJsonObject& jobj, plugin::PluginData& data ) const
0167 {
0168     QString type = jobj["type"].toString();
0169 
0170     if ( type == "action" )
0171     {
0172         std::unique_ptr<ActionService> act = std::make_unique<ActionService>();
0173         act->script = load_script(jobj["script"].toObject());
0174         if ( !act->script.valid() )
0175         {
0176             logger.stream() << "Skipping action with invalid script";
0177             return;
0178         }
0179         act->label = jobj["label"].toString();
0180         act->tooltip = jobj["tooltip"].toString();
0181         act->icon = jobj["icon"].toString();
0182         if ( act->icon.isEmpty() )
0183             act->icon = data.icon;
0184         data.services.emplace_back(std::move(act));
0185     }
0186     else if ( type == "format" )
0187     {
0188         auto svc = std::make_unique<IoService>();
0189         svc->save = load_script(jobj["save"].toObject());
0190         svc->open = load_script(jobj["open"].toObject());
0191         if ( !svc->save.valid() && !svc->open.valid() )
0192         {
0193             logger.stream() << "Skipping format service with no open nor save";
0194             return;
0195         }
0196 
0197         svc->label = jobj["name"].toString();
0198         for ( auto extv : jobj["extensions"].toArray() )
0199         {
0200             QString ext = extv.toString();
0201             if ( ext.startsWith(".") )
0202             {
0203                 logger.stream() << "Format extensions should not have the leading dot";
0204                 ext = ext.mid(1);
0205             }
0206 
0207             if ( ext.isEmpty() )
0208             {
0209                 logger.stream() << "Empty extension";
0210                 continue;
0211             }
0212 
0213             svc->extensions.push_back(ext);
0214         }
0215 
0216         if ( svc->extensions.isEmpty() )
0217         {
0218             logger.stream() << "Skipping format service with no extensions";
0219             return;
0220         }
0221 
0222         svc->auto_open = jobj["auto_open"].toBool(true);
0223 
0224         svc->slug = jobj["slug"].toString();
0225         if ( svc->slug.isEmpty() )
0226             svc->slug = svc->extensions[0];
0227 
0228         data.services.emplace_back(std::move(svc));
0229     }
0230     else
0231     {
0232         logger.stream() << "Skipping invalid service type" << type;
0233     }
0234 
0235 
0236 }
0237 
0238 plugin::PluginScript plugin::PluginRegistry::load_script ( const QJsonObject& jobj ) const
0239 {
0240     PluginScript s;
0241     s.module = jobj["module"].toString();
0242     s.function = jobj["function"].toString();
0243     QJsonArray settings = jobj["settings"].toArray();
0244     for ( auto setting : settings )
0245     {
0246         load_setting(setting.toObject(), s);
0247     }
0248 
0249     return s;
0250 }
0251 
0252 void plugin::PluginRegistry::load_setting (const QJsonObject& jobj, plugin::PluginScript& script ) const
0253 {
0254     QString type = jobj["type"].toString();
0255     QString slug = jobj["name"].toString();
0256     if ( slug.isEmpty() )
0257     {
0258         logger.stream() << "Skipping setting with no name";
0259         return;
0260     }
0261     QString label = jobj["label"].toString(slug);
0262     QString description = jobj["description"].toString();
0263     QVariant default_value = jobj["default"].toVariant();
0264 
0265     if ( type == "info" )
0266         script.settings.emplace_back(slug, label, description);
0267     else if ( type == "bool" )
0268         script.settings.emplace_back(slug, label, description, default_value.toBool());
0269     else if ( type == "int" )
0270         script.settings.emplace_back(slug, label, description, default_value.toInt(), jobj["min"].toInt(), jobj["max"].toInt());
0271     else if ( type == "float" )
0272         script.settings.emplace_back(slug, label, description, default_value.toFloat(), jobj["min"].toDouble(), jobj["max"].toDouble());
0273     else if ( type == "string" )
0274         script.settings.emplace_back(slug, label, description, default_value.toString());
0275     else if ( type == "choice" )
0276         script.settings.emplace_back(slug, label, description, app::settings::Setting::String, default_value, load_choices(jobj["choices"]));
0277     else if ( type == "color" )
0278         script.settings.emplace_back(slug, label, description, app::settings::Setting::Color, default_value);
0279     else
0280         logger.stream() << "Unknown type" << type << "for plugin setting" << slug;
0281 }
0282 
0283 QVariantMap plugin::PluginRegistry::load_choices ( const QJsonValue& val ) const
0284 {
0285     QVariantMap ret;
0286 
0287     if ( val.isObject() )
0288     {
0289         QJsonObject obj = val.toObject();
0290         for ( auto it = obj.begin(); it != obj.end(); ++it )
0291             ret[it.key()] = it->toVariant();
0292     }
0293     else if ( val.isArray() )
0294     {
0295         for ( auto i : val.toArray() )
0296         {
0297             QVariant v = i.toVariant();
0298             ret[v.toString()] = v;
0299         }
0300     }
0301 
0302     return ret;
0303 }
0304 
0305 
0306 plugin::Plugin * plugin::PluginRegistry::plugin ( const QString& id ) const
0307 {
0308     auto it = names.find(id);
0309     if ( it == names.end() )
0310         return {};
0311     return plugins_[*it].get();
0312 }
0313 
0314 void plugin::PluginRegistry::set_executor(plugin::Executor* exec)
0315 {
0316     executor_ = exec;
0317 }
0318 
0319 plugin::Executor * plugin::PluginRegistry::executor() const
0320 {
0321     return executor_;
0322 }
0323 
0324 QVariant plugin::PluginRegistry::global_parameter(const QString& name) const
0325 {
0326     if ( !executor_ )
0327         return {};
0328     return executor_->get_global(name);
0329 }