diff --git a/src/libs/utils/infobar.cpp b/src/libs/utils/infobar.cpp index dc5fb4878d2..2821419116c 100644 --- a/src/libs/utils/infobar.cpp +++ b/src/libs/utils/infobar.cpp @@ -264,10 +264,20 @@ void InfoBarDisplay::infoBarDestroyed() // will delete the widgets itself) or setInfoBar() being called explicitly. } +static void disconnectRecursively(QObject *obj) +{ + obj->disconnect(); + for (QObject *child : obj->children()) + disconnectRecursively(child); +} + void InfoBarDisplay::update() { for (QWidget *widget : std::as_const(m_infoWidgets)) { - widget->disconnect(this); // We want no destroyed() signal now + // Make sure that we are no longer connect to anything (especially lambdas). + // Otherwise a lambda might live longer than the owner of the lambda. + disconnectRecursively(widget); + widget->hide(); // Late deletion can cause duplicate infos. Hide immediately to prevent it. widget->deleteLater(); } diff --git a/src/plugins/lua/bindings/fetch.cpp b/src/plugins/lua/bindings/fetch.cpp index dd165cf72f8..c369c2754be 100644 --- a/src/plugins/lua/bindings/fetch.cpp +++ b/src/plugins/lua/bindings/fetch.cpp @@ -4,6 +4,8 @@ #include "../luaengine.h" #include "../luatr.h" +#include "utils.h" + #include #include @@ -135,188 +137,199 @@ void setupFetchModule() std::shared_ptr module = std::make_shared(); - registerProvider("Fetch", [mod = std::move(module)](sol::state_view lua) -> sol::object { - const ScriptPluginSpec *pluginSpec = lua.get("PluginSpec"); + registerProvider( + "Fetch", + [mod = std::move(module), + infoBarCleaner = InfoBarCleaner()](sol::state_view lua) mutable -> 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 async = lua.script("return require('async')", "_fetch_").get(); + sol::function wrap = async["wrap"]; - sol::table fetch = lua.create_table(); + 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 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("Allow the extension \"%1\" to fetch from the following URL:\n%2") - .arg(pluginName) - .arg(url), - QMessageBox::Yes | QMessageBox::No, - 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; - } - - InfoBarEntry entry{ - Id("Fetch").withSuffix(pluginName), - Tr::tr("Allow the extension \"%1\" to fetch data from the internet?") - .arg(pluginName)}; - entry.setDetailsWidgetCreator([pluginName, url] { - const QString markdown = Tr::tr("Allow the extension \"%1\" to fetch data" - "from the following URL:\n\n") - .arg("**" + pluginName + "**") - + QString("* [%1](%1)").arg(url); - - QLabel *list = new QLabel(); - list->setTextFormat(Qt::TextFormat::MarkdownText); - list->setText(markdown); - list->setMargin(StyleHelper::SpacingTokens::ExPaddingGapS); - return list; - }); - entry.addCustomButton(Tr::tr("Always Allow"), [mod, pluginName, fetch]() { - mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes); - ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); - fetch(); - }); - entry.addCustomButton(Tr::tr("Allow Once"), [pluginName, fetch]() { - ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); - fetch(); - }); - - entry.setCancelButtonInfo(Tr::tr("Deny"), [mod, notAllowed, pluginName]() { - ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); - mod->setAllowedToFetch(pluginName, Module::IsAllowed::No); - notAllowed(); - }); - 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", {}); - bool convertToTable - = options.get>("convertToTable").value_or(false); - - QNetworkRequest request((QUrl(url))); - if (headers && !headers.empty()) { - for (const auto &[k, v] : headers) - request.setRawHeader(k.as().toUtf8(), v.as().toUtf8()); + auto checkPermission = [mod, + &infoBarCleaner, + 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; } - QNetworkReply *reply = nullptr; - if (method == "get") - reply = NetworkAccessManager::instance()->get(request); - else if (method == "post") - reply = NetworkAccessManager::instance()->post(request, data.toUtf8()); - else - throw std::runtime_error("Unknown method: " + method.toStdString()); + 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("Allow the extension \"%1\" to fetch from the following URL:\n%2") + .arg(pluginName) + .arg(url), + QMessageBox::Yes | QMessageBox::No, + ICore::dialogParent()); + msgBox->setCheckBox(new QCheckBox(Tr::tr("Remember choice"))); - if (convertToTable) { QObject::connect( - reply, &QNetworkReply::finished, guard, [reply, thisState, callback]() { - reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError) { - callback(QString("%1 (%2):\n%3") - .arg(reply->errorString()) - .arg(QString::fromLatin1( - QMetaEnum::fromType() - .valueToKey(reply->error()))) - .arg(QString::fromUtf8(reply->readAll()))); - return; - } - - QByteArray data = reply->readAll(); - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) { - callback(error.errorString()); - return; - } - - callback(toTable(thisState, doc)); + msgBox, &QMessageBox::accepted, guard, [mod, fetch, pluginName, msgBox]() { + if (msgBox->checkBox()->isChecked()) + mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes); + fetch(); }); - } else { QObject::connect( - reply, &QNetworkReply::finished, guard, [reply, callback]() { + msgBox, + &QMessageBox::rejected, + guard, + [mod, notAllowed, pluginName, msgBox]() { + if (msgBox->checkBox()->isChecked()) + mod->setAllowedToFetch(pluginName, Module::IsAllowed::No); + notAllowed(); + }); + + msgBox->show(); + + return; + } + + const Id infoBarId = Id("Fetch").withSuffix(pluginName); + infoBarCleaner.infoBarEntryAdded(infoBarId); + + InfoBarEntry entry{ + infoBarId, + Tr::tr("Allow the extension \"%1\" to fetch data from the internet?") + .arg(pluginName)}; + entry.setDetailsWidgetCreator([pluginName, url] { + const QString markdown = Tr::tr("Allow the extension \"%1\" to fetch data" + "from the following URL:\n\n") + .arg("**" + pluginName + "**") + + QString("* [%1](%1)").arg(url); + + QLabel *list = new QLabel(); + list->setTextFormat(Qt::TextFormat::MarkdownText); + list->setText(markdown); + list->setMargin(StyleHelper::SpacingTokens::ExPaddingGapS); + return list; + }); + entry.addCustomButton(Tr::tr("Always Allow"), [mod, pluginName, fetch]() { + mod->setAllowedToFetch(pluginName, Module::IsAllowed::Yes); + ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); + fetch(); + }); + entry.addCustomButton(Tr::tr("Allow Once"), [pluginName, fetch]() { + ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); + fetch(); + }); + + entry.setCancelButtonInfo(Tr::tr("Deny"), [mod, notAllowed, pluginName]() { + ICore::infoBar()->removeInfo(Id("Fetch").withSuffix(pluginName)); + mod->setAllowedToFetch(pluginName, Module::IsAllowed::No); + notAllowed(); + }); + ICore::infoBar()->addInfo(entry); + }; + + fetch["fetch_cb"] = [checkPermission, + pluginName = pluginSpec->name, + guard = pluginSpec->connectionGuard.get(), + mod]( + const sol::main_table &options, + const sol::main_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", {}); + bool convertToTable + = options.get>("convertToTable").value_or(false); + + QNetworkRequest request((QUrl(url))); + if (headers && !headers.empty()) { + for (const auto &[k, v] : headers) + request.setRawHeader(k.as().toUtf8(), v.as().toUtf8()); + } + + QNetworkReply *reply = nullptr; + if (method == "get") + reply = NetworkAccessManager::instance()->get(request); + else if (method == "post") + reply = NetworkAccessManager::instance()->post(request, data.toUtf8()); + else + throw std::runtime_error("Unknown method: " + method.toStdString()); + + if (convertToTable) { + QObject::connect( + reply, &QNetworkReply::finished, guard, [reply, thisState, callback]() { + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + callback( + QString("%1 (%2):\n%3") + .arg(reply->errorString()) + .arg(QString::fromLatin1( + QMetaEnum::fromType() + .valueToKey(reply->error()))) + .arg(QString::fromUtf8(reply->readAll()))); + return; + } + + QByteArray data = reply->readAll(); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + callback(error.errorString()); + return; + } + + callback(toTable(thisState, doc)); + }); + + } else { + QObject::connect(reply, &QNetworkReply::finished, guard, [reply, callback]() { // We don't want the network reply to be deleted by the manager, but // by the Lua GC reply->setParent(nullptr); callback(std::unique_ptr(reply)); }); - } + } + }; + + checkPermission(url, actualFetch, [callback, pluginName]() { + callback( + Tr::tr("Fetching is not allowed for the extension \"%1\". (You can edit " + "permissions in Preferences > Lua.)") + .arg(pluginName)); + }); }; - checkPermission(url, actualFetch, [callback, pluginName]() { - callback(Tr::tr("Fetching is not allowed for the extension \"%1\". (You can edit " - "permissions in Preferences > Lua.)") - .arg(pluginName)); - }); - }; + fetch["fetch"] = wrap(fetch["fetch_cb"]); - fetch["fetch"] = wrap(fetch["fetch_cb"]); - - return fetch; - }); + return fetch; + }); } } // namespace Lua::Internal diff --git a/src/plugins/lua/bindings/install.cpp b/src/plugins/lua/bindings/install.cpp index 38cea1f9fa3..b8b7be718ae 100644 --- a/src/plugins/lua/bindings/install.cpp +++ b/src/plugins/lua/bindings/install.cpp @@ -4,6 +4,8 @@ #include "../luaengine.h" #include "../luatr.h" +#include "utils.h" + #include #include #include @@ -255,7 +257,9 @@ void setupInstallModule() }; registerProvider( - "Install", [state = State()](sol::state_view lua) mutable -> sol::object { + "Install", + [state = State(), + infoBarCleaner = InfoBarCleaner()](sol::state_view lua) mutable -> sol::object { sol::table async = lua.script("return require('async')", "_install_async_").get(); sol::function wrap = async["wrap"]; @@ -281,7 +285,7 @@ void setupInstallModule() }; install["install_cb"] = - [pluginSpec, &state]( + [pluginSpec, &state, &infoBarCleaner]( const QString &msg, const sol::table &installOptions, const sol::function &callback) { @@ -354,6 +358,8 @@ void setupInstallModule() .withSuffix(pluginSpec->name) .withSuffix(QString::number(qHash(installOptionsList))); + infoBarCleaner.infoBarEntryAdded(infoBarId); + InfoBarEntry entry(infoBarId, msg, InfoBarEntry::GlobalSuppression::Enabled); entry.addCustomButton(Tr::tr("Install"), [install, infoBarId]() { diff --git a/src/plugins/lua/bindings/utils.cpp b/src/plugins/lua/bindings/utils.cpp index ca56340e9ba..c19ed97d819 100644 --- a/src/plugins/lua/bindings/utils.cpp +++ b/src/plugins/lua/bindings/utils.cpp @@ -4,6 +4,8 @@ #include "../luaengine.h" #include "../luaqttypes.h" +#include + #include "utils.h" #include @@ -12,6 +14,7 @@ #include #include #include +#include #include #include @@ -318,4 +321,10 @@ void setupUtilsModule() }); } +InfoBarCleaner::~InfoBarCleaner() +{ + for (const auto &id : openInfoBars) + Core::ICore::infoBar()->removeInfo(id); +} + } // namespace Lua::Internal diff --git a/src/plugins/lua/bindings/utils.h b/src/plugins/lua/bindings/utils.h index 3a1233b482b..13135e35ff4 100644 --- a/src/plugins/lua/bindings/utils.h +++ b/src/plugins/lua/bindings/utils.h @@ -7,6 +7,7 @@ #include #include +#include #include @@ -61,4 +62,13 @@ inline QFlags tableToFlags(const sol::table &table) noexcept { return flags; } +class InfoBarCleaner +{ + QList openInfoBars; + +public: + ~InfoBarCleaner(); + void infoBarEntryAdded(const Utils::Id &id) { openInfoBars.append(id); } +}; + } // namespace Lua::Internal