mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-07 15:51:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f140a1208 | ||
|
|
cb7d2c698d | ||
|
|
a38e6c61f2 |
@@ -311,7 +311,7 @@ function CreatePrinterBlock(OneModel)
|
||||
|
||||
return '<div class="PrinterBlock" onClick="ChooseModel(\''+vendor+'\',\''+OneModel['model']+'\')">'+
|
||||
' <div class="PImg">'+
|
||||
' <img class="ModelThumbnail" src="' + OneModel['cover'] + '" />'+
|
||||
' <img class="ModelThumbnail" src="' + OneModel['cover'] + '" onerror="this.onerror=null;this.src=\'../img/printer-dummy.png\';"/>'+
|
||||
' </div>'+
|
||||
' <div class="PrinterInfoMark">?</div>'+
|
||||
' <div class="PrinterInfo">'+
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -115,9 +115,6 @@ static const std::unordered_map<std::string, AuditEventCategory> audit_event_cat
|
||||
{"subprocess.Popen", AuditEventCategory::ProcessCreate},
|
||||
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
|
||||
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
|
||||
|
||||
// threading
|
||||
{"_thread.start_new_thread", AuditEventCategory::Threading},
|
||||
};
|
||||
|
||||
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
|
||||
@@ -738,12 +735,6 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
case AuditEventCategory::Threading:
|
||||
// Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant
|
||||
// persists per plugin: the permission list matches targets by exact string, and the
|
||||
// started function's repr embeds an address that changes every run.
|
||||
targets.emplace_back("thread");
|
||||
return targets;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -770,7 +761,6 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, Plugi
|
||||
case AuditEventCategory::Http: return &permissions.network_http;
|
||||
case AuditEventCategory::Socket: return &permissions.network_socket;
|
||||
case AuditEventCategory::ProcessCreate: return &permissions.process;
|
||||
case AuditEventCategory::Threading: return &permissions.threading;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -842,74 +832,11 @@ wxString audit_message(AuditEventCategory category, const wxString& plugin_name,
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::ProcessCreate:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
|
||||
case AuditEventCategory::Threading:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
|
||||
default:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Builds and shows the modal permission prompt. Must run on the GUI thread.
|
||||
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
|
||||
const std::vector<std::string>& unresolved)
|
||||
{
|
||||
wxString target_list;
|
||||
for (const auto& target : unresolved)
|
||||
target_list += wxString::FromUTF8(target.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(nullptr,
|
||||
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
|
||||
wxString::FromUTF8(event_name.c_str()), target_list),
|
||||
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
}
|
||||
|
||||
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
|
||||
// freshly because the async prompt outlives the caller's stack copy of it.
|
||||
void persist_grant(const std::string& plugin_key, AuditEventCategory category, const std::vector<std::string>& targets)
|
||||
{
|
||||
PluginInstallState state;
|
||||
if (!PluginManager::instance().get_install_state(plugin_key, state))
|
||||
return;
|
||||
|
||||
std::vector<std::string>* permission_list = permission_list_for(category, state.permissions);
|
||||
if (!permission_list)
|
||||
return;
|
||||
|
||||
for (const auto& target : targets)
|
||||
persist_permission(plugin_key, state, *permission_list, target);
|
||||
}
|
||||
|
||||
// Requests permission for an audited event, returning true when it is already granted or the user
|
||||
// approves an inline prompt.
|
||||
//
|
||||
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
|
||||
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
||||
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
|
||||
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
|
||||
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
|
||||
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
|
||||
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
|
||||
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
|
||||
// re-prompting.
|
||||
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
|
||||
const std::string& event_name, const std::vector<std::string>& unresolved)
|
||||
{
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
if (wxIsMainThread())
|
||||
return prompt_for_targets(category, plugin_name, event_name, unresolved);
|
||||
|
||||
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() {
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
if (prompt_for_targets(category, plugin_name, event_name, unresolved))
|
||||
persist_grant(plugin_key, category, unresolved);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
int decide_audited_event(PluginAuditManager& mgr,
|
||||
PluginInstallState& state,
|
||||
const std::string& plugin_key,
|
||||
@@ -930,7 +857,15 @@ int decide_audited_event(PluginAuditManager& mgr,
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved))
|
||||
wxString target_list;
|
||||
for (const auto& target : unresolved)
|
||||
target_list += wxString::FromUTF8(target.c_str()) + "\n";
|
||||
|
||||
wxMessageDialog dialog(nullptr,
|
||||
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
|
||||
wxString::FromUTF8(event_name.c_str()), target_list),
|
||||
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
|
||||
if (dialog.ShowModal() != wxID_YES)
|
||||
return report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (permission_list)
|
||||
|
||||
@@ -807,7 +807,6 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
|
||||
read_string_list("network_http", parsed.permissions.network_http);
|
||||
read_string_list("network_socket", parsed.permissions.network_socket);
|
||||
read_string_list("process", parsed.permissions.process);
|
||||
read_string_list("threading", parsed.permissions.threading);
|
||||
}
|
||||
|
||||
if (state.contains("enabled") && state["enabled"].is_boolean())
|
||||
@@ -851,7 +850,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
{"network_http", state.permissions.network_http},
|
||||
{"network_socket", state.permissions.network_socket},
|
||||
{"process", state.permissions.process},
|
||||
{"threading", state.permissions.threading},
|
||||
};
|
||||
|
||||
nlohmann::json capabilities = nlohmann::json::array();
|
||||
|
||||
@@ -25,11 +25,19 @@ extern const char* const INSTALL_STATE_FILE;
|
||||
// Plugin config and orca.host.ui payloads both cross the boundary as plain JSON-compatible
|
||||
// values, so both go through these.
|
||||
|
||||
inline pybind11::object json_to_py(const nlohmann::json& j)
|
||||
// Maximum nesting depth for JSON <-> Python conversion. A self-referential or pathologically
|
||||
// deep value would otherwise recurse until the native C stack overflows, an uncatchable crash;
|
||||
// past this bound we raise instead. 200 is far beyond any legitimate plugin config or UI payload.
|
||||
inline constexpr int kMaxJsonConversionDepth = 200;
|
||||
|
||||
inline pybind11::object json_to_py(const nlohmann::json& j, int depth = 0)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
if (depth > kMaxJsonConversionDepth)
|
||||
throw py::value_error("Plugin JSON value nested too deeply");
|
||||
|
||||
switch (j.type()) {
|
||||
case json::value_t::null: return py::none();
|
||||
case json::value_t::boolean: return py::bool_(j.get<bool>());
|
||||
@@ -40,24 +48,27 @@ inline pybind11::object json_to_py(const nlohmann::json& j)
|
||||
case json::value_t::array: {
|
||||
py::list lst;
|
||||
for (const auto& e : j)
|
||||
lst.append(json_to_py(e));
|
||||
lst.append(json_to_py(e, depth + 1));
|
||||
return lst;
|
||||
}
|
||||
case json::value_t::object: {
|
||||
py::dict d;
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
d[py::str(it.key())] = json_to_py(it.value());
|
||||
d[py::str(it.key())] = json_to_py(it.value(), depth + 1);
|
||||
return d;
|
||||
}
|
||||
default: return py::none();
|
||||
}
|
||||
}
|
||||
|
||||
inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
inline nlohmann::json py_to_json(const pybind11::handle& o, int depth = 0)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
using json = nlohmann::json;
|
||||
|
||||
if (depth > kMaxJsonConversionDepth)
|
||||
throw py::value_error("Plugin value nested too deeply (possible cycle)");
|
||||
|
||||
if (o.is_none())
|
||||
return json(nullptr);
|
||||
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
|
||||
@@ -73,13 +84,13 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
|
||||
if (py::isinstance<py::dict>(o)) {
|
||||
json obj = json::object();
|
||||
for (auto item : py::reinterpret_borrow<py::dict>(o))
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
|
||||
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second, depth + 1);
|
||||
return obj;
|
||||
}
|
||||
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
|
||||
json arr = json::array();
|
||||
for (auto e : o)
|
||||
arr.push_back(py_to_json(e));
|
||||
arr.push_back(py_to_json(e, depth + 1));
|
||||
return arr;
|
||||
}
|
||||
return py::str(o).cast<std::string>(); // fallback: str()
|
||||
@@ -92,7 +103,6 @@ struct PluginPermissions
|
||||
std::vector<std::string> network_http;
|
||||
std::vector<std::string> network_socket;
|
||||
std::vector<std::string> process;
|
||||
std::vector<std::string> threading;
|
||||
};
|
||||
|
||||
struct PluginInstallState {
|
||||
|
||||
@@ -34,6 +34,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_plugin_sort.cpp
|
||||
test_plugin_cloud_metadata.cpp
|
||||
test_plugin_audit.cpp
|
||||
test_plugin_json_depth.cpp
|
||||
test_shortcuts.cpp
|
||||
test_file_url.cpp
|
||||
test_user_manager.cpp
|
||||
|
||||
@@ -122,7 +122,6 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
|
||||
state.permissions.network_http = {"https://api.example.com"};
|
||||
state.permissions.network_socket = {"192.168.45.6:443"};
|
||||
state.permissions.process = {"/usr/bin/curl"};
|
||||
state.permissions.threading = {"thread"};
|
||||
REQUIRE(write_install_state(plugin_dir, state));
|
||||
|
||||
// Permission data is persisted in the same sidecar as the installation metadata.
|
||||
@@ -133,7 +132,6 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
|
||||
CHECK(persisted.permissions.network_http == state.permissions.network_http);
|
||||
CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
|
||||
CHECK(persisted.permissions.process == state.permissions.process);
|
||||
CHECK(persisted.permissions.threading == state.permissions.threading);
|
||||
|
||||
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
|
||||
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/gil.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
// Brings the embedded interpreter up for one test and tears it down before boost::log does,
|
||||
// mirroring the ScopedPluginManager idiom in the other plugin tests.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
ScopedDataDir python_data_dir{"plugin-json-depth"};
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
~ScopedPluginManager()
|
||||
{
|
||||
PluginManager::instance().shutdown();
|
||||
PythonInterpreter::instance().shutdown();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("py_to_json raises instead of overflowing on pathologically deep input", "[PluginHost][Python]")
|
||||
{
|
||||
ScopedPluginManager manager;
|
||||
REQUIRE(manager.initialized);
|
||||
namespace py = pybind11;
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
// [[[ ... 0 ... ]]] nested 300 deep: past the 200 conversion-depth cap, but shallow enough
|
||||
// that the pre-fix code returns without crashing, so a regression fails cleanly rather than
|
||||
// taking the process down. Built in C++ so the test does not depend on Python builtins.
|
||||
py::object deep = py::int_(0);
|
||||
for (int i = 0; i < 300; ++i) {
|
||||
py::list wrapper;
|
||||
wrapper.append(deep);
|
||||
deep = std::move(wrapper);
|
||||
}
|
||||
CHECK_THROWS_AS(py_to_json(deep), std::exception);
|
||||
}
|
||||
|
||||
TEST_CASE("py_to_json still converts reasonably nested input", "[PluginHost][Python]")
|
||||
{
|
||||
ScopedPluginManager manager;
|
||||
REQUIRE(manager.initialized);
|
||||
namespace py = pybind11;
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
py::dict d;
|
||||
d["a"] = py::int_(1);
|
||||
py::list inner;
|
||||
inner.append(py::str("x"));
|
||||
inner.append(py::int_(2));
|
||||
d["b"] = inner;
|
||||
|
||||
const nlohmann::json j = py_to_json(d);
|
||||
CHECK(j.at("a").get<std::int64_t>() == 1);
|
||||
CHECK(j.at("b").at(0).get<std::string>() == "x");
|
||||
CHECK(j.at("b").at(1).get<std::int64_t>() == 2);
|
||||
}
|
||||
Reference in New Issue
Block a user