mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
* Escape Project Metadata in the Project Page and Restrict Accessory Opening The Project page rendered the model and profile name, author, description and accessory file names from the 3MF as live HTML. Names, authors and file names are now set as text, and the file list is built from DOM nodes with bound click handlers instead of concatenated markup. Descriptions can legitimately carry rich-text HTML, so they are rebuilt from an inert DOMParser document, keeping only plain formatting tags, http(s) links and http(s) images, with every other attribute dropped. Opening an accessory from the page now only launches regular files that lie inside the project's extracted auxiliary directory. The containment check is a new libslic3r helper, is_absolute_path_within_root, built on is_path_within_root so symlinks leading out of the root are rejected too. * Tighten Project Page Description Rendering and Keep More Formatting Link and image URLs in descriptions must now start with an http or https scheme as written and parse as such with the URL parser. Preview images are built as DOM nodes like the file list, and accessory names show their full text as a tooltip. Descriptions keep more plain formatting: del, ins, figure, figcaption, dl, dt, dd, caption, q, abbr, kbd and wbr, plus alt, title, width and height on images, colspan and rowspan on table cells and start on ordered lists. Numeric attributes must be plain integers. Embedded YouTube players become a link to the video. * Confirm Before Opening Program Attachments and Load Only HTTPS Images Opening a project attachment whose type runs as a program or script (executables, installers, shortcuts, shell and PowerShell scripts, macOS command files and apps, Linux desktop entries) now asks for confirmation first. The check lives in libslic3r as is_executable_file_name and ignores the trailing dots and spaces Windows strips from file names. Images in project descriptions are kept only when they load over https, so opening the Project tab no longer issues plain-http requests. * Open Project Attachments Through One Guarded Helper The Edit Project Info view launched attachments directly, without the checks the project page has. Both now call desktop_open_project_attachment, which checks that the file is inside the auxiliary directory, asks for confirmation where needed and then opens it. The auxiliary root was built through encode_path, which returns code page bytes on Windows, while boost::filesystem reads a narrow string as UTF-8. With a non-ASCII temporary directory the root never matched and no attachment opened. It is now built from the UTF-8 path directly. The list of program extensions could not be kept complete and let unknown types open without a prompt. It is replaced by is_safe_to_open_file_name, a list of plain document, image, model and video types that open directly. Everything else asks first.
505 lines
16 KiB
C++
505 lines
16 KiB
C++
#include "Tab.hpp"
|
|
#include "Project.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "libslic3r/Model.hpp"
|
|
#include "libslic3r/Format/bbs_3mf.hpp"
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
#include <wx/app.h>
|
|
#include <wx/button.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/sizer.h>
|
|
|
|
#include <wx/bmpcbox.h>
|
|
#include <wx/bmpbuttn.h>
|
|
#include <wx/treectrl.h>
|
|
#include <wx/imaglist.h>
|
|
#include <wx/settings.h>
|
|
#include <wx/filedlg.h>
|
|
#include <wx/wupdlock.h>
|
|
#include <wx/dataview.h>
|
|
#include <wx/tokenzr.h>
|
|
#include <wx/arrstr.h>
|
|
#include <wx/tglbtn.h>
|
|
|
|
#include "wxExtensions.hpp"
|
|
#include "GUI_App.hpp"
|
|
#include "GUI_ObjectList.hpp"
|
|
#include "MainFrame.hpp"
|
|
#include <slic3r/GUI/Widgets/WebView.hpp>
|
|
|
|
namespace Slic3r { namespace GUI {
|
|
|
|
wxDEFINE_EVENT(EVT_PROJECT_RELOAD, wxCommandEvent);
|
|
|
|
const std::vector<std::string> license_list = {
|
|
"BSD License",
|
|
"Apache License",
|
|
"GPL License",
|
|
"LGPL License",
|
|
"MIT License",
|
|
"CC License"
|
|
};
|
|
|
|
ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style) : wxPanel(parent, id, pos, size, style)
|
|
{
|
|
SetBackgroundColour(*wxWHITE);
|
|
m_project_home_url = file_url_from_path(boost::filesystem::path(resources_dir()) / "web/model/index.html");
|
|
wxString strlang = wxGetApp().current_language_code_safe();
|
|
if (strlang != "")
|
|
m_project_home_url += "?lang=" + strlang;
|
|
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
m_browser = WebView::CreateWebView(this, m_project_home_url);
|
|
if (m_browser == nullptr) {
|
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("load web view of project page failed");
|
|
return;
|
|
}
|
|
//m_browser->Hide();
|
|
main_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
|
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &ProjectPanel::on_navigated, this);
|
|
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &ProjectPanel::OnScriptMessage, this, m_browser->GetId());
|
|
Bind(wxEVT_WEBVIEW_NAVIGATING, &ProjectPanel::onWebNavigating, this, m_browser->GetId());
|
|
|
|
Bind(EVT_PROJECT_RELOAD, &ProjectPanel::on_reload, this);
|
|
|
|
m_auxiliary = new AuxiliaryPanel(this);
|
|
m_auxiliary->Hide();
|
|
main_sizer->Add(m_auxiliary, wxSizerFlags().Expand().Proportion(1));
|
|
add_build_steps_of(*m_auxiliary);
|
|
Bind(EVT_AUXILIARY_DONE, [this](wxCommandEvent& e) { update_model_data();});
|
|
|
|
SetSizer(main_sizer);
|
|
Layout();
|
|
Fit();
|
|
}
|
|
|
|
ProjectPanel::~ProjectPanel()
|
|
{
|
|
shutdown();
|
|
}
|
|
|
|
void ProjectPanel::shutdown()
|
|
{
|
|
m_reload_cancel_token->store(true, std::memory_order_release);
|
|
if (m_reload_task && m_reload_task->joinable())
|
|
m_reload_task->join();
|
|
m_reload_task.reset();
|
|
}
|
|
|
|
// Helper to convert newlines to <br>
|
|
static std::string convert_newlines_to_br(const std::string& text) {
|
|
std::string result = text;
|
|
size_t pos = 0;
|
|
while ((pos = result.find('\n', pos)) != std::string::npos) {
|
|
result.replace(pos, 1, "<br>");
|
|
pos += 4;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ProjectPanel::onWebNavigating(wxWebViewEvent& evt)
|
|
{
|
|
wxString tmpUrl = evt.GetURL();
|
|
//wxString NowUrl = m_browser->GetCurrentURL();
|
|
|
|
if (boost::starts_with(tmpUrl, "http://") || boost::starts_with(tmpUrl, "https://")) {
|
|
m_browser->Stop();
|
|
evt.Veto();
|
|
wxLaunchDefaultApplication(tmpUrl);
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::on_reload(wxCommandEvent& evt)
|
|
{
|
|
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
|
m_reload_cancel_token->load(std::memory_order_acquire))
|
|
return;
|
|
|
|
if (m_reload_task && m_reload_task->joinable())
|
|
m_reload_task->join();
|
|
|
|
const auto cancel_token = m_reload_cancel_token;
|
|
m_reload_task = std::make_unique<boost::thread>([this, cancel_token] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
std::string update_type;
|
|
std::string license;
|
|
std::string model_name;
|
|
std::string model_author;
|
|
std::string cover_file;
|
|
std::string description;
|
|
std::string p_name;
|
|
std::string p_author;
|
|
std::string p_description;
|
|
std::string p_cover_file;
|
|
|
|
std::map<std::string, std::vector<json>> files;
|
|
|
|
if (wxGetApp().plater() == nullptr)
|
|
return;
|
|
|
|
Model model = wxGetApp().plater()->model();
|
|
|
|
auto model_info = model.model_info;
|
|
if (model_info != nullptr) {
|
|
license = model_info->license;
|
|
model_name = model_info->model_name;
|
|
cover_file = model_info->cover_file;
|
|
description = model_info->description;
|
|
update_type = model_info->origin;
|
|
|
|
try {
|
|
if (!model_info->copyright.empty()) {
|
|
json copy_right = json::parse(model_info->copyright);
|
|
|
|
if (copy_right.is_array()) {
|
|
for (auto it = copy_right.begin(); it != copy_right.end(); it++) {
|
|
if ((*it).contains("author")) {
|
|
model_author = (*it)["author"].get<std::string>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
;
|
|
}
|
|
}
|
|
|
|
if (model_author.empty() && model.design_info != nullptr)
|
|
model_author = model.design_info->Designer;
|
|
|
|
if (model.profile_info != nullptr) {
|
|
p_name = model.profile_info->ProfileTile;
|
|
p_description = model.profile_info->ProfileDescription;
|
|
p_cover_file = model.profile_info->ProfileCover;
|
|
p_author = model.profile_info->ProfileUserName;
|
|
}
|
|
|
|
// file info
|
|
std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
|
|
if (!file_path.empty()) {
|
|
files = Reload(file_path);
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
|
|
wxGetApp().CallAfter([this, cancel_token, file_path, files] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
m_auxiliary->Reload(file_path, files);
|
|
});
|
|
} else {
|
|
clear_model_info();
|
|
return;
|
|
}
|
|
|
|
bool has_content = false;
|
|
for (const string& v : {
|
|
update_type,
|
|
license,
|
|
model_name,
|
|
model_author,
|
|
cover_file,
|
|
description,
|
|
p_name,
|
|
p_author,
|
|
p_description,
|
|
p_cover_file,
|
|
}) {
|
|
if (!v.empty()) {
|
|
has_content = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_content) {
|
|
for (const auto & file : files) {
|
|
if (!file.second.empty()) {
|
|
has_content = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!has_content) {
|
|
// Nothing to show, just return
|
|
clear_model_info();
|
|
return;
|
|
}
|
|
|
|
json j;
|
|
j["model"]["license"] = license;
|
|
j["model"]["name"] = wxGetApp().url_encode(model_name);
|
|
j["model"]["author"] = wxGetApp().url_encode(model_author);;
|
|
j["model"]["cover_img"] = wxGetApp().url_encode(cover_file);
|
|
j["model"]["description"] = wxGetApp().url_encode(convert_newlines_to_br(description));
|
|
j["model"]["preview_img"] = files["Model Pictures"];
|
|
j["model"]["upload_type"] = update_type;
|
|
|
|
j["file"]["BOM"] = files["Bill of Materials"];
|
|
j["file"]["Assembly"] = files["Assembly Guide"];
|
|
j["file"]["Other"] = files["Others"];
|
|
|
|
j["profile"]["name"] = wxGetApp().url_encode(p_name);
|
|
j["profile"]["author"] = wxGetApp().url_encode(p_author);
|
|
j["profile"]["description"] = wxGetApp().url_encode(p_description);
|
|
j["profile"]["cover_img"] = wxGetApp().url_encode(p_cover_file);
|
|
j["profile"]["preview_img"] = files["Profile Pictures"];
|
|
|
|
json m_Res = json::object();
|
|
m_Res["command"] = "show_3mf_info";
|
|
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
|
m_Res["model"] = j;
|
|
|
|
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
|
|
|
if (m_web_init_completed.load(std::memory_order_acquire) &&
|
|
!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
|
|
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
RunScript(strJS.ToStdString());
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void ProjectPanel::msw_rescale()
|
|
{
|
|
m_auxiliary->msw_rescale();
|
|
}
|
|
|
|
void ProjectPanel::on_size(wxSizeEvent &event)
|
|
{
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectPanel::on_navigated(wxWebViewEvent& event)
|
|
{
|
|
event.Skip();
|
|
}
|
|
|
|
void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
|
{
|
|
try {
|
|
wxString strInput = evt.GetString();
|
|
json j = json::parse(strInput.utf8_string());
|
|
|
|
wxString strCmd = j["command"];
|
|
|
|
if (strCmd == "open_3mf_accessory") {
|
|
wxString accessory_path = j["accessory_path"];
|
|
|
|
if (!accessory_path.empty()) {
|
|
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
|
fs::path path(decode_path);
|
|
if (!desktop_open_project_attachment(this, path))
|
|
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path;
|
|
}
|
|
}
|
|
else if (strCmd == "request_3mf_info") {
|
|
m_web_init_completed.store(true, std::memory_order_release);
|
|
}
|
|
else if (strCmd == "edit_project_info") {
|
|
show_info_editor(true);
|
|
}
|
|
else if (strCmd == "debug_info") {
|
|
//wxString msg = j["msg"];
|
|
//OutputDebugString(wxString::Format("Model_Web: msg = %s \r\n", msg));
|
|
//BOOST_LOG_TRIVIAL(info) << wxString::Format("Model_Web: msg = %s", msg);
|
|
}
|
|
|
|
}
|
|
catch (std::exception&) {
|
|
// wxMessageBox(e.what(), "json Exception", MB_OK);
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::show_info_editor(bool show)
|
|
{
|
|
m_browser->Show(!show);
|
|
m_auxiliary->Show(show);
|
|
Layout();
|
|
}
|
|
|
|
void ProjectPanel::update_model_data()
|
|
{
|
|
Model model = wxGetApp().plater()->model();
|
|
show_info_editor(false);
|
|
clear_model_info();
|
|
|
|
m_auxiliary->init_auxiliary();
|
|
|
|
//basics info
|
|
//if (model.model_info == nullptr)
|
|
// return;
|
|
|
|
auto event = wxCommandEvent(EVT_PROJECT_RELOAD);
|
|
event.SetEventObject(this);
|
|
wxPostEvent(this, event);
|
|
}
|
|
|
|
void ProjectPanel::clear_model_info()
|
|
{
|
|
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
|
|
m_reload_cancel_token->load(std::memory_order_acquire))
|
|
return;
|
|
|
|
json m_Res = json::object();
|
|
m_Res["command"] = "clear_3mf_info";
|
|
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
|
|
|
|
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
|
|
|
|
const auto cancel_token = m_reload_cancel_token;
|
|
wxGetApp().CallAfter([this, cancel_token, strJS] {
|
|
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
|
|
return;
|
|
RunScript(strJS.ToStdString());
|
|
});
|
|
}
|
|
|
|
std::map<std::string, std::vector<json>> ProjectPanel::Reload(wxString aux_path)
|
|
{
|
|
std::vector<fs::path> dir_cache;
|
|
fs::directory_iterator iter_end;
|
|
std::map<std::string, std::vector<json>> m_paths_list;
|
|
|
|
const static std::array<wxString, 5> s_default_folders = {
|
|
("Model Pictures"),
|
|
("Bill of Materials"),
|
|
("Assembly Guide"),
|
|
("Others"),
|
|
//(".thumbnails"),
|
|
("Profile Pictures"),
|
|
};
|
|
|
|
for (auto folder : s_default_folders)
|
|
m_paths_list[folder.ToStdString()] = std::vector<json>{};
|
|
|
|
|
|
fs::path new_aux_path(aux_path.ToStdWstring());
|
|
|
|
// Check new path. If not exist, create a new one.
|
|
if (!fs::exists(new_aux_path)) {
|
|
fs::create_directory(new_aux_path);
|
|
}
|
|
|
|
// Create default folders if they are not loaded
|
|
for (auto folder : s_default_folders) {
|
|
wxString folder_path = aux_path + "/" + folder;
|
|
if (fs::exists(folder_path.ToStdWstring())) continue;
|
|
fs::create_directory(folder_path.ToStdWstring());
|
|
}
|
|
|
|
// Load from new path
|
|
for (fs::directory_iterator iter(new_aux_path); iter != iter_end; iter++) {
|
|
wxString path = iter->path().generic_wstring();
|
|
dir_cache.push_back(iter->path());
|
|
}
|
|
|
|
|
|
for (auto dir : dir_cache) {
|
|
for (fs::directory_iterator iter(dir); iter != iter_end; iter++) {
|
|
if (fs::is_directory(iter->path())) continue;
|
|
|
|
json pfile_obj;
|
|
|
|
std::string file_path = iter->path().string();
|
|
fs::path file_path_obj = fs::path(iter->path().string());
|
|
|
|
for (auto folder : s_default_folders) {
|
|
auto idx = file_path.find(folder.ToStdString());
|
|
if (idx != std::string::npos) {
|
|
|
|
wxStructStat strucStat;
|
|
wxString file_name = encode_path(file_path.c_str());
|
|
wxStat(file_name, &strucStat);
|
|
wxFileOffset filelen = strucStat.st_size;
|
|
|
|
pfile_obj["_filepath"] = file_path;
|
|
pfile_obj["filename"] = wxGetApp().url_encode(file_path_obj.filename().string().c_str());
|
|
pfile_obj["size"] = formatBytes((unsigned long)filelen);
|
|
|
|
std::string file_extension = file_path_obj.extension().string();
|
|
boost::algorithm::to_lower(file_extension);
|
|
|
|
//image
|
|
if (file_extension == ".jpg" ||
|
|
file_extension == ".jpeg" ||
|
|
file_extension == ".pjpeg" ||
|
|
file_extension == ".png" ||
|
|
file_extension == ".jfif" ||
|
|
file_extension == ".pjp" ||
|
|
file_extension == ".webp" ||
|
|
file_extension == ".bmp")
|
|
{
|
|
|
|
wxString base64_str = to_base64(file_path);
|
|
pfile_obj["filepath"] = base64_str.ToStdString();
|
|
m_paths_list[folder.ToStdString()].push_back(pfile_obj);
|
|
break;
|
|
}
|
|
else {
|
|
pfile_obj["filepath"] = wxGetApp().url_encode(file_path);
|
|
m_paths_list[folder.ToStdString()].push_back(pfile_obj);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return m_paths_list;
|
|
}
|
|
|
|
std::string ProjectPanel::formatBytes(unsigned long bytes)
|
|
{
|
|
double dValidData = round(double(bytes) / (1024 * 1024) * 1000) / 1000;
|
|
return wxString::Format("%.2fMB", dValidData).ToStdString();
|
|
}
|
|
|
|
wxString ProjectPanel::to_base64(std::string file_path)
|
|
{
|
|
|
|
std::ifstream imageFile(encode_path(file_path.c_str()), std::ios::binary);
|
|
if (!imageFile) {
|
|
return wxEmptyString;
|
|
}
|
|
|
|
std::ostringstream imageStream;
|
|
imageStream << imageFile.rdbuf();
|
|
|
|
std::string binaryImageData = imageStream.str();
|
|
|
|
std::string extension;
|
|
size_t last_dot = file_path.find_last_of(".");
|
|
|
|
if (last_dot != std::string::npos) {
|
|
extension = file_path.substr(last_dot + 1);
|
|
}
|
|
|
|
wxString bease64_head = wxString::Format("data:image/%s;base64,", extension);
|
|
|
|
|
|
std::wstringstream wss;
|
|
wss << bease64_head;
|
|
wss << wxBase64Encode(binaryImageData.data(), binaryImageData.size());
|
|
|
|
wxString base64_str = wss.str();
|
|
return base64_str;
|
|
}
|
|
|
|
void ProjectPanel::RunScript(std::string content)
|
|
{
|
|
WebView::RunScript(m_browser, content);
|
|
}
|
|
|
|
bool ProjectPanel::Show(bool show)
|
|
{
|
|
if (show) update_model_data();
|
|
return wxPanel::Show(show);
|
|
}
|
|
|
|
}} // namespace Slic3r::GUI
|