diff --git a/src/plugins/lua/CMakeLists.txt b/src/plugins/lua/CMakeLists.txt index 253d6f06ee6..c7a62e3a8b8 100644 --- a/src/plugins/lua/CMakeLists.txt +++ b/src/plugins/lua/CMakeLists.txt @@ -42,6 +42,12 @@ add_qtc_plugin(Lua # generateqtbindings.cpp # Use this if you need to generate some code. ) +qt_add_resources(Lua lua_images_rcc + PREFIX "/lua" + FILES + images/settingscategory_lua.png + images/settingscategory_lua@2x.png +) set_source_files_properties(luauibindings.cpp PROPERTY SKIP_AUTOMOC ON PROPERTY SKIP_AUTOGEN ON) diff --git a/src/plugins/lua/bindings/fetch.cpp b/src/plugins/lua/bindings/fetch.cpp index 35e3255943f..22d1bc64cbd 100644 --- a/src/plugins/lua/bindings/fetch.cpp +++ b/src/plugins/lua/bindings/fetch.cpp @@ -3,11 +3,22 @@ #include "../luaengine.h" #include "../luaqttypes.h" +#include "../luatr.h" +#include +#include + +#include +#include +#include #include +#include +#include +#include #include #include +#include #include #include #include @@ -37,35 +48,205 @@ static QString opToString(QNetworkAccessManager::Operation op) void addFetchModule() { - LuaEngine::registerProvider( - "Fetch", [](sol::state_view lua) -> sol::object { - const ScriptPluginSpec *pluginSpec = lua.get("PluginSpec"); + class Module : Utils::AspectContainer + { + Utils::StringListAspect pluginsAllowedToFetch{this}; + Utils::StringListAspect pluginsNotAllowedToFetch{this}; - sol::table async = lua.script("return require('async')", "_fetch_").get(); - sol::function wrap = async["wrap"]; + class LuaOptionsPage : public Core::IOptionsPage + { + public: + LuaOptionsPage(Module *module) + { + setId("BB.Lua.Fetch"); + setDisplayName(Tr::tr("Network access")); + setCategory("ZY.Lua"); + setDisplayCategory("Lua"); + setCategoryIconPath(":/lua/images/settingscategory_lua.png"); + setSettingsProvider( + [module] { return static_cast(module); }); + } + }; - sol::table fetch = lua.create_table(); + LuaOptionsPage settingsPage{this}; - auto networkReplyType = lua.new_usertype( - "QNetworkReply", - "error", - sol::property([](QNetworkReply *self) -> int { return self->error(); }), - "readAll", - [](QNetworkReply *r) { return r->readAll().toStdString(); }, - "__tostring", - [](QNetworkReply *r) { - return QString("QNetworkReply(%1 \"%2\") => %3") - .arg(opToString(r->operation())) - .arg(r->url().toString()) - .arg(r->error()); - }); + public: + Module() + { + setSettingsGroup("Lua.Fetch"); - fetch["fetch_cb"] = [guard = pluginSpec->connectionGuard.get()]( - const sol::table &options, - const sol::function &callback, - const sol::this_state &thisState) { - auto url = options.get("url"); + pluginsAllowedToFetch.setSettingsKey("pluginsAllowedToFetch"); + pluginsAllowedToFetch.setLabelText("Plugins allowed to fetch data from the internet"); + pluginsAllowedToFetch.setToolTip( + "List of plugins that are allowed to fetch data from the internet"); + pluginsAllowedToFetch.setUiAllowAdding(false); + pluginsAllowedToFetch.setUiAllowEditing(false); + pluginsNotAllowedToFetch.setSettingsKey("pluginsNotAllowedToFetch"); + pluginsNotAllowedToFetch.setLabelText( + "Plugins not allowed to fetch data from the internet"); + pluginsNotAllowedToFetch.setToolTip( + "List of plugins that are not allowed to fetch data from the internet"); + pluginsNotAllowedToFetch.setUiAllowAdding(false); + pluginsNotAllowedToFetch.setUiAllowEditing(false); + + setLayouter([this] { + using namespace Layouting; + // clang-format off + return Form { + pluginsAllowedToFetch, br, + pluginsNotAllowedToFetch, br, + }; + // clang-format on + }); + + readSettings(); + } + + ~Module() { writeSettings(); } + + enum class IsAllowed { Yes, No, NeedsToAsk }; + + IsAllowed isAllowedToFetch(const QString &pluginName) const + { + if (pluginsAllowedToFetch().contains(pluginName)) + return IsAllowed::Yes; + if (pluginsNotAllowedToFetch().contains(pluginName)) + return IsAllowed::No; + return IsAllowed::NeedsToAsk; + } + + void setAllowedToFetch(const QString &pluginName, IsAllowed allowed) + { + if (allowed == IsAllowed::Yes) + pluginsAllowedToFetch.appendValue(pluginName); + else if (allowed == IsAllowed::No) + pluginsNotAllowedToFetch.appendValue(pluginName); + + if (allowed == IsAllowed::Yes) + pluginsNotAllowedToFetch.removeValue(pluginName); + else if (allowed == IsAllowed::No) + pluginsAllowedToFetch.removeValue(pluginName); + } + }; + + std::shared_ptr module = std::make_shared(); + + LuaEngine::registerProvider("Fetch", [mod = std::move(module)](sol::state_view lua) -> sol::object { + const ScriptPluginSpec *pluginSpec = lua.get("PluginSpec"); + + sol::table async = lua.script("return require('async')", "_fetch_").get(); + sol::function wrap = async["wrap"]; + + sol::table fetch = lua.create_table(); + + auto networkReplyType = lua.new_usertype( + "QNetworkReply", + "error", + sol::property([](QNetworkReply *self) -> int { return self->error(); }), + "readAll", + [](QNetworkReply *r) { return r->readAll().toStdString(); }, + "__tostring", + [](QNetworkReply *r) { + return QString("QNetworkReply(%1 \"%2\") => %3") + .arg(opToString(r->operation())) + .arg(r->url().toString()) + .arg(r->error()); + }); + + auto checkPermission = [mod, + pluginName = pluginSpec->name, + guard = pluginSpec->connectionGuard.get()]( + QString url, + std::function fetch, + std::function notAllowed) { + auto isAllowed = mod->isAllowedToFetch(pluginName); + if (isAllowed == Module::IsAllowed::Yes) { + fetch(); + return; + } + + if (isAllowed == Module::IsAllowed::No) { + notAllowed(); + return; + } + + if (QApplication::activeModalWidget()) { + // We are already showing a modal dialog, + // so we have to use a QMessageBox instead of the info bar + auto msgBox = new QMessageBox( + QMessageBox::Question, + Tr::tr("Allow Internet access"), + Tr::tr("The plugin \"%1\" would like to fetch from the following url:\n%2") + .arg(pluginName) + .arg(url), + QMessageBox::Yes | QMessageBox::No, + Core::ICore::dialogParent()); + msgBox->setCheckBox(new QCheckBox(Tr::tr("Remember choice"))); + + QObject::connect( + msgBox, &QMessageBox::accepted, guard, [mod, fetch, pluginName, msgBox]() { + if (msgBox->checkBox()->isChecked()) + mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes); + fetch(); + }); + + QObject::connect( + msgBox, &QMessageBox::rejected, guard, [mod, notAllowed, pluginName, msgBox]() { + if (msgBox->checkBox()->isChecked()) + mod->setAllowedToFetch(pluginName, Module::IsAllowed::No); + notAllowed(); + }); + + msgBox->show(); + + return; + } + + Utils::InfoBarEntry entry{ + Utils::Id::fromString("Fetch" + pluginName), + Tr::tr("The plugin \"%1\" would like to fetch data from the internet. Do " + "you want to allow this?") + .arg(pluginName)}; + entry.setDetailsWidgetCreator([pluginName, url] { + const QString markdown = Tr::tr("The plugin \"**%1**\" would like to fetch " + "from the following url:\n\n") + .arg(pluginName) + + QString("* [%3](%3)").arg(url); + + QLabel *list = new QLabel(); + list->setTextFormat(Qt::TextFormat::MarkdownText); + list->setText(markdown); + list->setMargin(Utils::StyleHelper::SpacingTokens::ExPaddingGapS); + return list; + }); + entry.addCustomButton(Tr::tr("Always Allow"), [mod, pluginName, fetch]() { + mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes); + Core::ICore::infoBar()->removeInfo(Utils::Id::fromString("Fetch" + pluginName)); + fetch(); + }); + entry.addCustomButton(Tr::tr("Allow once"), [pluginName, fetch]() { + Core::ICore::infoBar()->removeInfo(Utils::Id::fromString("Fetch" + pluginName)); + fetch(); + }); + + entry.setCancelButtonInfo(Tr::tr("Deny"), [mod, notAllowed, pluginName]() { + Core::ICore::infoBar()->removeInfo(Utils::Id::fromString("Fetch" + pluginName)); + mod->setAllowedToFetch(pluginName, Module::IsAllowed::No); + notAllowed(); + }); + Core::ICore::infoBar()->addInfo(entry); + }; + + fetch["fetch_cb"] = [checkPermission, + pluginName = pluginSpec->name, + guard = pluginSpec->connectionGuard.get(), + mod]( + const sol::table &options, + const sol::function &callback, + const sol::this_state &thisState) { + auto url = options.get("url"); + auto actualFetch = [guard, url, options, callback, thisState]() { auto method = (options.get_or("method", "GET")).toLower(); auto headers = options.get_or("headers", {}); auto data = options.get_or("body", {}); @@ -129,10 +310,17 @@ void addFetchModule() } }; - fetch["fetch"] = wrap(fetch["fetch_cb"]); + checkPermission(url, actualFetch, [callback, pluginName]() { + callback(Tr::tr("Fetching is not allowed for the plugin \"%1\" (You can edit " + "permissions in Preferences => Lua)") + .arg(pluginName)); + }); + }; - return fetch; - }); + fetch["fetch"] = wrap(fetch["fetch_cb"]); + + return fetch; + }); } } // namespace Lua::Internal diff --git a/src/plugins/lua/images/settingscategory_lua.png b/src/plugins/lua/images/settingscategory_lua.png new file mode 100644 index 00000000000..7f1b6808fea Binary files /dev/null and b/src/plugins/lua/images/settingscategory_lua.png differ diff --git a/src/plugins/lua/images/settingscategory_lua@2x.png b/src/plugins/lua/images/settingscategory_lua@2x.png new file mode 100644 index 00000000000..a70400124db Binary files /dev/null and b/src/plugins/lua/images/settingscategory_lua@2x.png differ