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 }