mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0aa4c2768 | ||
|
|
68fe3475cd | ||
|
|
950f1631e9 | ||
|
|
e2911ac6e6 | ||
|
|
a8f35ed484 |
@@ -548,6 +548,20 @@ body
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
/*---- Cloud mode: OrcaCloud is a native pane to the right of the app panel, so this page
|
||||
is reduced to its left nav column (the class is toggled by SetCloudMode in js/home.js). ----*/
|
||||
body.cloud-mode #RightBoard
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.cloud-mode #LeftBoard
|
||||
{
|
||||
/*---- 262px of column plus its 1px separator is the width the app gives the nav. ----*/
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#ContentBoard
|
||||
{
|
||||
overflow-y:auto;
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="BtnIcon "><img class="LeftIcon" src="img/i2.png" /></div>
|
||||
<div data-i18n="recent">Recent</div>
|
||||
</div>
|
||||
<div class="BtnItem BtnShortcut" onClick="OpenUrlInLocalBrowser('https://cloud.orcaslicer.com')">
|
||||
<div class="BtnItem BtnShortcut" onClick="GotoCloud()">
|
||||
<div class="BtnIcon "><img class="ShortcutBrandIcon" src="../image/logo.png" /></div>
|
||||
<div class="ShortcutTextRow">
|
||||
<div>OrcaCloud</div>
|
||||
|
||||
@@ -145,8 +145,36 @@ function HandleStudio( pVal )
|
||||
}
|
||||
}
|
||||
|
||||
/*---- OrcaCloud opens as a native pane beside this page, so the shell only switches its
|
||||
own cloud-mode state and asks the app to show or hide that pane (WebViewPanel::set_cloud_mode). ----*/
|
||||
var m_CloudMode=false;
|
||||
|
||||
function SetCloudMode( bOn )
|
||||
{
|
||||
if( m_CloudMode==bOn )
|
||||
return;
|
||||
|
||||
m_CloudMode=bOn;
|
||||
$("body").toggleClass("cloud-mode", bOn);
|
||||
}
|
||||
|
||||
function GotoCloud()
|
||||
{
|
||||
$(".BtnItem").removeClass("BtnItemSelected");
|
||||
$(".BtnShortcut").addClass("BtnItemSelected");
|
||||
SetCloudMode(true);
|
||||
SendSimpleCommand("homepage_open_cloud");
|
||||
}
|
||||
|
||||
function GotoMenu( strMenu )
|
||||
{
|
||||
/*---- Any board in the right region means leaving the cloud pane. ----*/
|
||||
if( m_CloudMode )
|
||||
{
|
||||
SetCloudMode(false);
|
||||
SendSimpleCommand("homepage_close_cloud");
|
||||
}
|
||||
|
||||
let MenuList=$(".BtnItem");
|
||||
let nAll=MenuList.length;
|
||||
|
||||
|
||||
+149
-8
@@ -41,6 +41,7 @@
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#include <boost/log/detail/native_typeof.hpp>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
@@ -325,6 +326,13 @@ namespace GUI {
|
||||
|
||||
class MainFrame;
|
||||
|
||||
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
|
||||
// available (e.g. stealth mode or the network plugin still loading).
|
||||
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
|
||||
{
|
||||
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
|
||||
}
|
||||
|
||||
void start_ping_test()
|
||||
{
|
||||
return;
|
||||
@@ -2272,7 +2280,9 @@ void GUI_App::init_networking_callbacks()
|
||||
if (return_code == 5) {
|
||||
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: the desktop lost its session on its own. Leave the webview's
|
||||
// own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
if (msg_dlg.ShowModal() == wxOK) {
|
||||
return;
|
||||
@@ -5243,7 +5253,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
|
||||
{
|
||||
if (m_agent && m_agent->is_user_login(provider)) {
|
||||
m_agent->user_logout(true, provider);
|
||||
@@ -5256,6 +5266,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
}
|
||||
|
||||
if (provider == ORCA_CLOUD_PROVIDER) {
|
||||
// Invalidate any handoff still in flight, so a token minted for the signed-out
|
||||
// account is never loaded into the pane.
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
|
||||
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
|
||||
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
|
||||
// loop through the code prompt on every desktop re-login.
|
||||
if (user_initiated) {
|
||||
if (auto orca = get_orca_cloud_agent(m_agent)) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
|
||||
}
|
||||
}
|
||||
|
||||
/* delete old user settings */
|
||||
bool transfer_preset_changes = false;
|
||||
wxString header = _L("Some presets are modified.") + "\n" +
|
||||
@@ -5377,7 +5402,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
|
||||
request_user_logout();
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_orca_login_info") == 0) {
|
||||
@@ -5392,7 +5417,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_bambu_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
|
||||
@@ -5401,7 +5426,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_orca_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_modeldepot") == 0) {
|
||||
@@ -5413,6 +5438,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_openproject") == 0) {
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// Deferred: set_cloud_mode() may create a second native webview, which must
|
||||
// not run inside this webview's script-message callback.
|
||||
CallAfter([this]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(false);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_recent_projects") == 0) {
|
||||
if (mainframe) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
@@ -5685,7 +5724,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (!provider.empty() && m_agent->is_user_login(provider)) {
|
||||
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: a 401 means the desktop session was rejected. Leave the
|
||||
// webview's own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
|
||||
if (!m_show_http_error_msgdlg) {
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
@@ -5829,6 +5870,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
{
|
||||
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
|
||||
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
|
||||
// post request_web_session, which is what opens the desktop login dialog.
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
{
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
|
||||
m_cloud_handoff_state = state;
|
||||
const uint64_t gen = ++m_cloud_handoff_gen;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca || !orca->is_user_login()) {
|
||||
// Desktop signed out: open the desktop login dialog and stop here. On success
|
||||
// on_user_login_handle reloads the Start URL, which makes the page request again; on
|
||||
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
|
||||
// already stored, so a repeat request while the dialog is open is dropped rather than
|
||||
// stacking another dialog.
|
||||
if (!login_dlg || !login_dlg->IsShown())
|
||||
request_login(true, ORCA_CLOUD_PROVIDER);
|
||||
return;
|
||||
}
|
||||
|
||||
mint_cloud_web_session(state, gen);
|
||||
}
|
||||
|
||||
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
|
||||
{
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
|
||||
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
|
||||
boost::thread([this, orca, state, gen]() {
|
||||
std::string token_hash;
|
||||
std::string error_code;
|
||||
unsigned int http_code = 0;
|
||||
orca->request_web_session_handoff(token_hash, error_code, http_code);
|
||||
|
||||
if (is_closing())
|
||||
return;
|
||||
|
||||
if (token_hash.empty()) {
|
||||
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
|
||||
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
|
||||
// (it may echo account data), only the status/error code.
|
||||
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
|
||||
<< " error_code=" << error_code;
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
|
||||
on_cloud_web_session_minted(state, gen, token_hash);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
|
||||
{
|
||||
if (is_closing())
|
||||
return;
|
||||
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
|
||||
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
|
||||
return;
|
||||
if (token_hash.empty())
|
||||
return;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// Load the Complete URL in the SAME webview that posted request_web_session, because the
|
||||
// state lives in that webview's session storage (guide §3/§7).
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
|
||||
}
|
||||
|
||||
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
{
|
||||
if (!m_agent) { return; }
|
||||
@@ -5884,6 +6012,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
if (app_config->get("sync_user_preset") == "true") {
|
||||
start_sync_user_preset();
|
||||
}
|
||||
|
||||
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
|
||||
// without waiting for the next open. On a fresh login or account switch this invalidates any
|
||||
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
|
||||
// the page drops the old account and re-requests (guide §8).
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
}
|
||||
|
||||
|
||||
@@ -7952,8 +8090,11 @@ void GUI_App::on_stealth_mode_enter()
|
||||
{
|
||||
stop_sync_user_preset();
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
|
||||
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
|
||||
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
home->SendCloudProvidersInfo();
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ private:
|
||||
bool m_side_popup_status{false};
|
||||
bool m_show_http_error_msgdlg{false};
|
||||
std::chrono::steady_clock::time_point m_last_401_error_time;
|
||||
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
HttpServer m_http_server;
|
||||
@@ -540,7 +545,11 @@ public:
|
||||
|
||||
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
|
||||
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
|
||||
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
|
||||
// direction: a new caller that forgets the flag will not silently sign the webview out.
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
|
||||
void post_logout_to_webview(const std::string& provider);
|
||||
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
std::string handle_web_request(std::string cmd);
|
||||
@@ -556,6 +565,15 @@ public:
|
||||
void on_update_machine_list(wxCommandEvent& evt);
|
||||
void on_user_login(wxCommandEvent &evt);
|
||||
void on_user_login_handle(wxCommandEvent& evt);
|
||||
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
|
||||
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
void enable_user_preset_folder(bool enable);
|
||||
|
||||
// BBS
|
||||
|
||||
@@ -645,7 +645,7 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr
|
||||
combobox->SetSelection(current_region);
|
||||
return;
|
||||
} else {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
config->set("region", region.ToStdString());
|
||||
auto area = config->get_country_code();
|
||||
if (agent) {
|
||||
@@ -2479,7 +2479,7 @@ wxBoxSizer* PreferencesDialog::create_debug_page()
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
std::string country_code = config->get_country_code();
|
||||
if (agent) {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
agent->set_country_code(country_code);
|
||||
}
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"), ConfirmBeforeSendDialog::VisibleButtons::ONLY_CONFIRM); // ORCA VisibleButtons instead ButtonStyle
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "WebGuideDialog.hpp"
|
||||
#include "CloudProvider.hpp"
|
||||
#include "ConfigWizard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -753,7 +754,7 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
|
||||
if (wxGetApp().is_user_login()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: user_logout on user_guide_finish";
|
||||
// agent->user_logout();
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,17 @@
|
||||
#include <wx/textdlg.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/url.h>
|
||||
#include <wx/uri.h>
|
||||
|
||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebViewHostDialog.hpp>
|
||||
#include <wx/webview.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <webkit2/webkit2.h>
|
||||
#endif
|
||||
#include <wx/infobar.h>
|
||||
|
||||
namespace pt = boost::property_tree;
|
||||
@@ -55,6 +60,13 @@ namespace GUI {
|
||||
|
||||
#define LOGIN_INFO_UPDATE_TIMER_ID 10002
|
||||
|
||||
// OrcaCloud is a native pane of its own: the site refuses to be framed
|
||||
// (x-frame-options: DENY / CSP frame-ancestors 'none').
|
||||
static const char *const kCloudUrl = "https://cloud.orcaslicer.com";
|
||||
// #LeftBoard in resources/web/homepage/css/home.css is a 262px column plus a 1px right
|
||||
// border; the homepage keeps that width as the nav column while the cloud pane is open.
|
||||
static const int kLeftNavWidth = 263;
|
||||
|
||||
// The home page's strings (resources/web/homepage), keyed by their data-i18n and T() ids.
|
||||
static json home_page_strings()
|
||||
{
|
||||
@@ -134,7 +146,11 @@ WebViewPanel::WebViewPanel(wxWindow *parent)
|
||||
m_reset_on_show = WebView::NeedsRecreateOnShow();
|
||||
SetSizer(topsizer);
|
||||
|
||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
// The homepage shell is the left nav column; when OrcaCloud opens, its page lives in a
|
||||
// second native pane beside it (the site refuses to be framed).
|
||||
m_content_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_content_sizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
topsizer->Add(m_content_sizer, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
// Log backend information
|
||||
/* m_browser->GetUserAgent() may lead crash
|
||||
@@ -290,12 +306,282 @@ void WebViewPanel::create_browser()
|
||||
m_browser->Hide();
|
||||
}
|
||||
|
||||
// The cloud page gets its own native view, created on first use and kept alive afterwards
|
||||
// so its session and scroll position survive hiding.
|
||||
void WebViewPanel::create_cloud_browser()
|
||||
{
|
||||
m_cloud_browser = WebView::CreateWebView(this, wxEmptyString);
|
||||
|
||||
#ifdef __linux__
|
||||
// The pane's web session is a cookie (guide §3), and WebKitGTK keeps cookies only in memory
|
||||
// unless a persistent store is set up, so without this the sign-in is lost on every restart
|
||||
// (and on every reset_browser). Each wxWebView has its own context, so give the cloud pane its
|
||||
// own store instead of sharing PrinterWebView's. Mirrors PrinterWebView::create_browser.
|
||||
if (auto* wv = static_cast<WebKitWebView*>(m_cloud_browser->GetNativeBackend())) {
|
||||
const auto cookies_path = boost::filesystem::path(data_dir() + "/cache/cloud_cookies.db");
|
||||
auto* cookie_manager = webkit_web_context_get_cookie_manager(webkit_web_view_get_context(wv));
|
||||
webkit_cookie_manager_set_persistent_storage(cookie_manager, cookies_path.c_str(),
|
||||
WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Webview events are command events, so they would otherwise propagate up to the
|
||||
// panel's handlers, which drive the homepage view. Keep the cloud pane's events here.
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATING, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_LOADED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_TITLE_CHANGED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_ERROR, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
m_cloud_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewPanel::OnCloudWebViewEvent, this);
|
||||
|
||||
m_cloud_browser->Hide();
|
||||
if (m_content_sizer)
|
||||
m_content_sizer->Add(m_cloud_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
}
|
||||
|
||||
// The scheme://host[:port] origin of an http(s) URL, lower-cased and with the default port
|
||||
// dropped, or empty for any other scheme or a malformed URL. Comparing origins (not a string
|
||||
// prefix) is what keeps a lookalike host such as "https://cloud.orcaslicer.com.evil.com" out.
|
||||
static wxString http_origin_of(const wxString& url)
|
||||
{
|
||||
const wxURI uri(url);
|
||||
if (!uri.HasScheme() || !uri.HasServer())
|
||||
return {};
|
||||
const wxString scheme = uri.GetScheme().Lower();
|
||||
if (scheme != "http" && scheme != "https")
|
||||
return {};
|
||||
wxString origin = scheme + "://" + uri.GetServer().Lower();
|
||||
const wxString port = uri.GetPort();
|
||||
if (!port.IsEmpty() && port != (scheme == "https" ? "443" : "80"))
|
||||
origin += ":" + port;
|
||||
return origin;
|
||||
}
|
||||
|
||||
// Whether a NAVIGATING event will replace the pane's top-level document rather than load a
|
||||
// subframe. Edge and macOS report the target frame; WebKitGTK does not (IsTargetMainFrame() is
|
||||
// always false there), so a user-gesture navigation stands in: a page's own embedded frames load
|
||||
// without a gesture and stay put, while a user following a link to another site does not.
|
||||
static bool cloud_navigation_is_top_level(const wxWebViewEvent& evt)
|
||||
{
|
||||
#ifdef __WXGTK__
|
||||
return evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER;
|
||||
#else
|
||||
return evt.IsTargetMainFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Sends an off-site navigation to the system browser instead of embedding it. Deferred: this runs
|
||||
// inside a native webview navigation callback, so launching must not re-enter it.
|
||||
static void open_cloud_url_externally(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "cloud pane: opening externally " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
wxGetApp().CallAfter([url] { wxLaunchDefaultBrowser(url); });
|
||||
}
|
||||
|
||||
// Consumed on the cloud view so it never reaches the homepage handlers. {web} pages stay in the
|
||||
// pane; every other site (OAuth providers included) opens in the system browser (guide §3).
|
||||
void WebViewPanel::OnCloudWebViewEvent(wxWebViewEvent& evt)
|
||||
{
|
||||
const wxEventType event_type = evt.GetEventType();
|
||||
if (event_type == wxEVT_WEBVIEW_NAVIGATING) {
|
||||
const wxString url = evt.GetURL();
|
||||
// Guide §3: keep every {web} page in the pane and open other sites (OAuth providers
|
||||
// included) in the system browser. Only top-level navigations are diverted, and
|
||||
// should_divert_cloud_url() excludes same-origin URLs and internal schemes.
|
||||
if (should_divert_cloud_url(url) && cloud_navigation_is_top_level(evt)) {
|
||||
evt.Veto();
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
} else if (event_type == wxEVT_WEBVIEW_NEWWINDOW) {
|
||||
const wxString url = evt.GetURL();
|
||||
if (!url.empty()) {
|
||||
if (is_cloud_origin(url))
|
||||
load_cloud_url(url);
|
||||
else
|
||||
open_cloud_url_externally(url);
|
||||
}
|
||||
evt.Veto();
|
||||
} else if (event_type == wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED) {
|
||||
on_cloud_script_message(evt.GetString().utf8_string());
|
||||
} else if (event_type == wxEVT_WEBVIEW_LOADED || event_type == wxEVT_WEBVIEW_ERROR) {
|
||||
// A Start URL deferred behind a sign-out (see load_cloud_after_signout).
|
||||
if (!m_cloud_after_signout_url.IsEmpty()) {
|
||||
const wxString next = std::exchange(m_cloud_after_signout_url, wxString());
|
||||
load_cloud_url(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The page asks for a web session by posting JSON through window.wx; only the newest request
|
||||
// counts (tracked in GUI_App). Nothing here touches the UI directly: the desktop login dialog and
|
||||
// the network mint must not run on the webview's message-dispatch stack.
|
||||
void WebViewPanel::on_cloud_script_message(const std::string& body)
|
||||
{
|
||||
const json msg = json::parse(body, nullptr, false);
|
||||
if (msg.is_discarded() || !msg.is_object() || msg.value("command", std::string()) != "request_web_session")
|
||||
return;
|
||||
|
||||
std::string state;
|
||||
if (msg.contains("data") && msg["data"].is_object())
|
||||
state = msg["data"].value("state", std::string());
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
wxGetApp().CallAfter([state = std::move(state)] { wxGetApp().request_cloud_web_session(state); });
|
||||
}
|
||||
|
||||
void WebViewPanel::apply_cloud_layout(bool cloud_on)
|
||||
{
|
||||
if (!m_content_sizer)
|
||||
return;
|
||||
|
||||
// Cloud mode pins the homepage to the nav column width; otherwise it fills the panel.
|
||||
if (m_browser) {
|
||||
if (wxSizerItem* item = m_content_sizer->GetItem(m_browser))
|
||||
item->SetProportion(cloud_on ? 0 : 1);
|
||||
// kLeftNavWidth is in DIP: the web view's CSS px are DIP, so scale to physical pixels.
|
||||
m_browser->SetMinSize(cloud_on ? m_browser->FromDIP(wxSize(kLeftNavWidth, -1)) : wxDefaultSize);
|
||||
}
|
||||
Layout();
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_url(const wxString& url)
|
||||
{
|
||||
if (url.empty())
|
||||
return;
|
||||
// Track the origin of whatever the pane is told to load (Start URL, Complete URL, sign-out or
|
||||
// the plain cloud fallback), so navigation policy has the right origin from the first load on.
|
||||
const wxString origin = http_origin_of(url);
|
||||
if (!origin.IsEmpty())
|
||||
m_cloud_base_url = origin;
|
||||
if (!m_cloud_browser)
|
||||
return;
|
||||
// Raw LoadURL: WebView::LoadUrl re-serialises through wxURI, which must not alter the
|
||||
// fragment that carries the one-time handoff token. Log only the redacted URL.
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(url).ToUTF8().data();
|
||||
m_cloud_browser->LoadURL(url);
|
||||
}
|
||||
|
||||
bool WebViewPanel::is_cloud_origin(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
return !origin.IsEmpty() && !m_cloud_base_url.IsEmpty() && origin == m_cloud_base_url;
|
||||
}
|
||||
|
||||
bool WebViewPanel::should_divert_cloud_url(const wxString& url) const
|
||||
{
|
||||
const wxString origin = http_origin_of(url);
|
||||
// Unknown cloud origin or a non-web scheme: never divert (the pane must keep working).
|
||||
if (origin.IsEmpty() || m_cloud_base_url.IsEmpty())
|
||||
return false;
|
||||
return origin != m_cloud_base_url;
|
||||
}
|
||||
|
||||
// Explicit sign-out ends the pane's own web session. That session is a cookie JS cannot clear
|
||||
// (HttpOnly, guide §3/§8) and ClearHistory() is a no-op on WebKitGTK, so drop the pane's stored
|
||||
// data natively. wx 3.3.2 ignores the type mask on every backend (any non-zero mask clears
|
||||
// everything) and needs an invalid `since` to delete cookies, so the default "all" call is used.
|
||||
void WebViewPanel::clear_cloud_storage()
|
||||
{
|
||||
if (m_cloud_browser)
|
||||
m_cloud_browser->ClearBrowsingData(wxWEBVIEW_BROWSING_DATA_ALL);
|
||||
}
|
||||
|
||||
void WebViewPanel::load_cloud_after_signout(const wxString& url)
|
||||
{
|
||||
if (m_cloud_pending_signout && !m_cloud_signout_url.IsEmpty()) {
|
||||
// The pane did not exist when the user signed out, so its persisted session must be
|
||||
// cleared before the sign-out page runs and `url` loads once that navigation finishes
|
||||
// (see OnCloudWebViewEvent).
|
||||
m_cloud_pending_signout = false;
|
||||
m_cloud_after_signout_url = url;
|
||||
clear_cloud_storage();
|
||||
load_cloud_url(m_cloud_signout_url);
|
||||
return;
|
||||
}
|
||||
load_cloud_url(url);
|
||||
}
|
||||
|
||||
void WebViewPanel::schedule_cloud_load(const wxString& url)
|
||||
{
|
||||
if (!m_cloud_browser) {
|
||||
load_cloud_after_signout(url);
|
||||
return;
|
||||
}
|
||||
// CreateWebView queues the "wx" handler add on the view (macOS/Linux); queue the load behind
|
||||
// it so the page's first scripts already see the bridge (guide §3).
|
||||
m_cloud_browser->CallAfter([this, url]() { load_cloud_after_signout(url); });
|
||||
}
|
||||
|
||||
void WebViewPanel::cloud_signed_out(const wxString& signout_url)
|
||||
{
|
||||
if (signout_url.empty())
|
||||
return;
|
||||
m_cloud_signout_url = signout_url;
|
||||
// The next Start URL must re-seed the pane (it now reflects the signed-out state), so forget
|
||||
// the previous seed regardless of which branch below runs.
|
||||
m_cloud_seed_url.clear();
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_pending_signout = false;
|
||||
// Run the sign-out page (ends the server-side session), then clear the pane's stored data.
|
||||
load_cloud_url(signout_url);
|
||||
clear_cloud_storage();
|
||||
} else {
|
||||
// Pane never opened this run: clear the persisted session on its first open.
|
||||
m_cloud_pending_signout = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
|
||||
{
|
||||
if (on) {
|
||||
// load_cloud_url records the origin from whatever it loads, including the plain cloud
|
||||
// fallback used when `url` is empty.
|
||||
if (!m_cloud_browser) {
|
||||
create_cloud_browser();
|
||||
m_cloud_seed_url = url;
|
||||
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
|
||||
} else if (!url.empty() && url != m_cloud_seed_url) {
|
||||
// Re-seed only when the Start URL actually changed — a login, an account switch or a
|
||||
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
|
||||
// Start URL re-runs the page's auth check on every open, which flashes "Signing you
|
||||
// in…" and re-requests a session (guide §10: mint when the page asks, not per open).
|
||||
// The pane is kept alive, so an unchanged open just shows the page it already holds.
|
||||
m_cloud_seed_url = url;
|
||||
load_cloud_after_signout(url);
|
||||
}
|
||||
m_cloud_browser->Show();
|
||||
} else if (m_cloud_browser) {
|
||||
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
|
||||
// again (guide §10). Nothing is cleared here.
|
||||
m_cloud_browser->Hide();
|
||||
}
|
||||
apply_cloud_layout(on && m_cloud_browser != nullptr);
|
||||
}
|
||||
|
||||
void WebViewPanel::reset_browser()
|
||||
{
|
||||
// Rebuild both views the same way, keeping the cloud pane's open/closed state.
|
||||
const bool cloud_on = m_cloud_browser && m_cloud_browser->IsShown();
|
||||
|
||||
m_browser->Destroy(); // also removes it from the sizer
|
||||
create_browser();
|
||||
GetSizer()->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
Layout();
|
||||
m_content_sizer->Insert(size_t(0), m_browser, wxSizerFlags().Expand().Proportion(1));
|
||||
|
||||
if (m_cloud_browser) {
|
||||
m_cloud_browser->Destroy();
|
||||
m_cloud_browser = nullptr;
|
||||
create_cloud_browser();
|
||||
schedule_cloud_load(wxString(kCloudUrl));
|
||||
m_cloud_browser->Show(cloud_on);
|
||||
// The rebuilt pane holds no seed, so the next open loads the Start URL again.
|
||||
m_cloud_seed_url.clear();
|
||||
}
|
||||
|
||||
apply_cloud_layout(cloud_on);
|
||||
}
|
||||
|
||||
bool WebViewPanel::Show(bool show)
|
||||
@@ -645,7 +931,7 @@ void WebViewPanel::update_mode()
|
||||
*/
|
||||
void WebViewPanel::OnNavigationRequest(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
const wxString &url = evt.GetURL();
|
||||
if (url.StartsWith("File://") || url.StartsWith("file://")) {
|
||||
if (!url.Contains("/web/homepage/index.html")) {
|
||||
@@ -691,7 +977,7 @@ void WebViewPanel::OnNavigationComplete(wxWebViewEvent& evt)
|
||||
{
|
||||
m_browser->Show();
|
||||
Layout();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
|
||||
UpdateState();
|
||||
@@ -710,6 +996,9 @@ void WebViewPanel::OnDocumentLoaded(wxWebViewEvent& evt)
|
||||
if (wxGetApp().get_mode() == comDevelop)
|
||||
wxLogMessage("%s", "Document loaded; url='" + evt.GetURL() + "'");
|
||||
}
|
||||
// A reloaded homepage loses its cloud-mode class; restore it if the pane is open.
|
||||
if (m_cloud_browser && m_cloud_browser->IsShown())
|
||||
RunScript("SetCloudMode(true)");
|
||||
UpdateState();
|
||||
SendCloudProvidersInfo();
|
||||
}
|
||||
@@ -725,7 +1014,7 @@ void WebViewPanel::OnTitleChanged(wxWebViewEvent &evt)
|
||||
*/
|
||||
void WebViewPanel::OnNewWindow(wxWebViewEvent& evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << evt.GetURL().ToUTF8().data();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << WebView::RedactUrl(evt.GetURL()).ToUTF8().data();
|
||||
wxString flag = " (other)";
|
||||
|
||||
if (evt.GetNavigationAction() == wxWEBVIEW_NAV_ACTION_USER)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "wx/fs_mem.h"
|
||||
#include "wx/stdpaths.h"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/tbarbase.h>
|
||||
#include "wx/textctrl.h"
|
||||
#include <wx/timer.h>
|
||||
@@ -114,14 +115,62 @@ public:
|
||||
|
||||
void update_mode();
|
||||
|
||||
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
|
||||
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
|
||||
// whether it already has a session; empty falls back to the plain cloud page.
|
||||
void set_cloud_mode(bool on, const wxString& url = wxString());
|
||||
|
||||
// True while the OrcaCloud pane exists and is visible.
|
||||
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
|
||||
|
||||
// Loads `url` in the cloud pane — the webview that owns the session handoff. Raw LoadURL (no
|
||||
// wxURI re-serialisation) so the Complete URL's #fragment stays intact; never logged.
|
||||
void load_cloud_url(const wxString& url);
|
||||
|
||||
// Clears the cloud pane's own session on an explicit desktop sign-out. If the pane has not
|
||||
// been created yet, the sign-out is remembered and applied when it is first opened.
|
||||
void cloud_signed_out(const wxString& signout_url);
|
||||
|
||||
bool Show(bool show = true) override;
|
||||
private:
|
||||
void create_browser();
|
||||
void create_cloud_browser();
|
||||
void reset_browser();
|
||||
// Consumes the cloud pane's webview events so they stay off the homepage handlers.
|
||||
void OnCloudWebViewEvent(wxWebViewEvent& evt);
|
||||
// True when `url` has the same scheme/host/port as the cloud origin ({web}).
|
||||
bool is_cloud_origin(const wxString& url) const;
|
||||
// True when a navigation to `url` must leave the pane: an http(s) URL on another origin than
|
||||
// the (known) cloud origin. False for non-web schemes and while the origin is unknown.
|
||||
bool should_divert_cloud_url(const wxString& url) const;
|
||||
// Parses a request_web_session message and defers it to GUI_App.
|
||||
void on_cloud_script_message(const std::string& body);
|
||||
// Loads `url`, first clearing a lazily-required sign-out (see cloud_signed_out).
|
||||
void load_cloud_after_signout(const wxString& url);
|
||||
// First load of a freshly created pane: deferred so CreateWebView's queued "wx" handler add
|
||||
// runs first and the page sees window.wx at load time (guide §3).
|
||||
void schedule_cloud_load(const wxString& url);
|
||||
// Clears the pane's stored web data (its cookie-backed session) on an explicit sign-out.
|
||||
void clear_cloud_storage();
|
||||
// Re-applies the content sizer's proportions and min sizes for the cloud state.
|
||||
void apply_cloud_layout(bool cloud_on);
|
||||
|
||||
wxWebView* m_browser;
|
||||
wxWebView* m_cloud_browser{nullptr};
|
||||
wxBoxSizer* m_content_sizer{nullptr};
|
||||
wxString m_home_url;
|
||||
bool m_reset_on_show{false};
|
||||
// {web} origin (scheme://host[:port]) of the loaded pane. Set from every load, including the
|
||||
// plain cloud fallback, so navigation policy can keep {web} pages in the pane and send other
|
||||
// sites to the system browser.
|
||||
wxString m_cloud_base_url;
|
||||
// Start URL the pane was last seeded with. An open whose URL matches this is an ordinary
|
||||
// re-open (nothing changed), so it only shows the pane instead of reloading it (guide §10).
|
||||
wxString m_cloud_seed_url;
|
||||
// Sign-out clear the pane must apply once it exists / next loads.
|
||||
wxString m_cloud_signout_url;
|
||||
wxString m_cloud_after_signout_url;
|
||||
bool m_cloud_pending_signout{false};
|
||||
wxButton * m_button_stop;
|
||||
wxTextCtrl *m_url;
|
||||
#if !BBL_RELEASE_TO_PUBLIC
|
||||
|
||||
@@ -418,6 +418,12 @@ bool WebView::DownloadAndInstallWebViewRuntime()
|
||||
return DownloadAndInstallWV2RT() == 0;
|
||||
}
|
||||
#endif
|
||||
wxString WebView::RedactUrl(const wxString& url)
|
||||
{
|
||||
const size_t cut = url.find_first_of(wxString("?#"));
|
||||
return cut == wxString::npos ? url : url.Left(cut);
|
||||
}
|
||||
|
||||
void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
{
|
||||
auto url2 = url;
|
||||
@@ -425,7 +431,7 @@ void WebView::LoadUrl(wxWebView * webView, wxString const &url)
|
||||
url2.Replace("\\", "/");
|
||||
#endif
|
||||
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << url2.ToUTF8();
|
||||
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << RedactUrl(url2).ToUTF8();
|
||||
webView->LoadURL(url2);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public:
|
||||
#endif
|
||||
static void LoadUrl(wxWebView * webView, wxString const &url);
|
||||
|
||||
// Returns the URL truncated at the first '?' or '#', so a query/fragment carrying secrets
|
||||
// (e.g. a session-handoff token) is never written to a log.
|
||||
static wxString RedactUrl(const wxString& url);
|
||||
|
||||
static bool RunScript(wxWebView * webView, wxString const & msg);
|
||||
|
||||
// Marks "wx" as registered so CreateWebView's deferred add skips the duplicate.
|
||||
|
||||
@@ -482,6 +482,14 @@ void Http::priv::http_perform()
|
||||
if (!postfields.empty()) {
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size());
|
||||
} else if (method == "POST" && form == nullptr && mime == nullptr) {
|
||||
// A POST with no body. Without CURLOPT_POSTFIELDS, curl takes the request body from
|
||||
// the read callback, but form_file_read_cb only has a usable user pointer for a PUT
|
||||
// (set_put_body) or a streamed multipart part (form_add_file); for an empty POST it
|
||||
// dereferences libcurl's default READDATA as a form_file and crashes. Point curl at a
|
||||
// zero-length body so the read callback is never used.
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields.c_str());
|
||||
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(0));
|
||||
}
|
||||
|
||||
CURLcode res = ::curl_easy_perform(curl);
|
||||
|
||||
@@ -108,6 +108,10 @@ constexpr const char* ORCA_CLOUD_PRINTER = "/api/v1/printers";
|
||||
|
||||
constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login";
|
||||
|
||||
// Gateway endpoint that mints a one-time web-session handoff token for the signed-in desktop
|
||||
// user. The cloud pane reloads the returned token into the webview (see build_web_session_*).
|
||||
constexpr const char* ORCA_WEB_SESSION_HANDOFF_PATH = "/api/v1/account/web-session-handoff";
|
||||
|
||||
constexpr const char* CONFIG_ORCA_API_URL = "orca_api_url";
|
||||
constexpr const char* CONFIG_ORCA_AUTH_URL = "orca_auth_url";
|
||||
constexpr const char* CONFIG_ORCA_CLOUD_URL = "orca_cloud_url";
|
||||
@@ -3018,6 +3022,148 @@ std::string OrcaCloudServiceAgent::get_cloud_login_url(const std::string& langua
|
||||
return url;
|
||||
}
|
||||
|
||||
// Percent-encodes every byte outside the RFC 3986 unreserved set (A-Za-z0-9-._~), so token values
|
||||
// survive fragment parsing intact. Deliberately tiny: no URL-encoder dependency.
|
||||
static std::string percent_encode_unreserved(const std::string& s)
|
||||
{
|
||||
static const char* const hex = "0123456789ABCDEF";
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (unsigned char c : s) {
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '~') {
|
||||
out.push_back(static_cast<char>(c));
|
||||
} else {
|
||||
out.push_back('%');
|
||||
out.push_back(hex[c >> 4]);
|
||||
out.push_back(hex[c & 0x0F]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_start_url(const std::string& next) const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
|
||||
// The page replaces a `next` that is not a same-site path with /app/profiles, so pass a
|
||||
// same-site path (the guide's default) and let it validate.
|
||||
std::string url = cloud_base_url + "/auth/handoff?next=" +
|
||||
percent_encode_unreserved(next.empty() ? "/app/profiles" : next);
|
||||
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
if (session.logged_in && !session.user_id.empty())
|
||||
url += "&user_id=" + percent_encode_unreserved(session.user_id);
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_complete_url(const std::string& token_hash,
|
||||
const std::string& state) const
|
||||
{
|
||||
if (cloud_base_url.empty() || token_hash.empty() || state.empty())
|
||||
return "";
|
||||
|
||||
// Fragment (not query) so the one-time token never reaches a server or a log.
|
||||
return cloud_base_url + "/auth/handoff#token_hash=" + percent_encode_unreserved(token_hash) +
|
||||
"&state=" + percent_encode_unreserved(state);
|
||||
}
|
||||
|
||||
std::string OrcaCloudServiceAgent::build_web_session_signout_url() const
|
||||
{
|
||||
if (cloud_base_url.empty())
|
||||
return "";
|
||||
return cloud_base_url + "/auth/handoff?signout=1";
|
||||
}
|
||||
|
||||
// Extracts the machine-readable code from a handoff error body. 401 uses {"error":"<code>"};
|
||||
// the other statuses use {"error":{"code":"<code>","message":"..."}}.
|
||||
static std::string handoff_error_code(const std::string& body)
|
||||
{
|
||||
const json j = json::parse(body, nullptr, false);
|
||||
if (j.is_discarded() || !j.is_object() || !j.contains("error"))
|
||||
return "";
|
||||
const auto& err = j["error"];
|
||||
if (err.is_string())
|
||||
return err.get<std::string>();
|
||||
if (err.is_object() && err.contains("code") && err["code"].is_string())
|
||||
return err["code"].get<std::string>();
|
||||
return "";
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::request_web_session_handoff(std::string& token_hash, std::string& error_code,
|
||||
unsigned int& http_code)
|
||||
{
|
||||
token_hash.clear();
|
||||
error_code.clear();
|
||||
http_code = 0;
|
||||
|
||||
const std::string url = api_base_url + ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << ORCA_WEB_SESSION_HANDOFF_PATH;
|
||||
|
||||
// Refresh the desktop token the usual way before minting (newest access token).
|
||||
ensure_token_fresh("web_session_handoff");
|
||||
|
||||
auto perform = [&]() {
|
||||
HttpResult result;
|
||||
try {
|
||||
auto http = Http::post(url);
|
||||
http.tls_verify(true);
|
||||
|
||||
const std::string token = get_access_token();
|
||||
|
||||
auto headers = data_headers();
|
||||
for (const auto& pair : headers) {
|
||||
http.header(pair.first, pair.second);
|
||||
}
|
||||
if (!token.empty()) {
|
||||
http.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
|
||||
// No request body: the endpoint identifies the user by the bearer token alone.
|
||||
http.on_complete([&](std::string resp_body, unsigned resp_status) {
|
||||
result.success = true;
|
||||
result.status = resp_status;
|
||||
result.body = resp_body;
|
||||
})
|
||||
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
|
||||
result.success = false;
|
||||
result.status = resp_status == 0 ? 404 : resp_status;
|
||||
result.body = resp_body;
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff HTTP error - " << error;
|
||||
})
|
||||
.timeout_max(30)
|
||||
.perform_sync();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: web-session-handoff exception - " << e.what();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
HttpResult res = perform();
|
||||
// Reuse the shared 401 policy (refresh once, then retry), exactly like http_get/http_post:
|
||||
// `suppress` is true for a transient refresh failure (keep the session and token, no logout)
|
||||
// and false when the refresh token was rejected — so a 401 that remains after the refresh
|
||||
// surfaces through the generic HTTP-error callback and drives the normal desktop logout.
|
||||
// Every other handoff status (409/429/503/transport) stays silent here, so the page can
|
||||
// offer "Try again" instead of the desktop showing a generic cloud error.
|
||||
const bool suppress = resolve_unauthorized(res, perform, "web_session_handoff");
|
||||
if (!suppress && res.status == 401)
|
||||
invoke_http_error_callback(res.status, res.body);
|
||||
|
||||
http_code = res.status;
|
||||
if (res.status == 200) {
|
||||
const json j = json::parse(res.body, nullptr, false);
|
||||
if (!j.is_discarded() && j.is_object() && j.contains("token_hash") && j["token_hash"].is_string())
|
||||
token_hash = j["token_hash"].get<std::string>();
|
||||
} else {
|
||||
error_code = handoff_error_code(res.body);
|
||||
}
|
||||
|
||||
return (res.success && res.status == 200) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED;
|
||||
}
|
||||
|
||||
int OrcaCloudServiceAgent::get_mw_user_preference(std::function<void(std::string)> callback)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_preference (stub)";
|
||||
|
||||
@@ -203,6 +203,28 @@ public:
|
||||
// ========================================================================
|
||||
std::string get_cloud_service_host() override;
|
||||
std::string get_cloud_login_url(const std::string& language = "") override;
|
||||
|
||||
// ========================================================================
|
||||
// OrcaCloud webview session handoff (OrcaCloud "Webview Session Handoff" guide)
|
||||
// ========================================================================
|
||||
// The cloud pane never receives the desktop's access/refresh tokens. Instead the page posts
|
||||
// request_web_session, the desktop mints a single-use token through the gateway and loads the
|
||||
// Complete URL; the site then signs itself in. These builders return "" when no cloud URL is
|
||||
// configured.
|
||||
//
|
||||
// Start URL: {web}/auth/handoff?next=<path>[&user_id=<desktop user id>]. `next` defaults to
|
||||
// /app/profiles; user_id is included only while signed in.
|
||||
std::string build_web_session_start_url(const std::string& next = std::string()) const;
|
||||
// Complete URL: {web}/auth/handoff#token_hash=<hash>&state=<state>. Both values URL-encoded.
|
||||
std::string build_web_session_complete_url(const std::string& token_hash, const std::string& state) const;
|
||||
// Clears only the webview's own session; loaded on an explicit desktop sign-out.
|
||||
std::string build_web_session_signout_url() const;
|
||||
// POST {api}/api/v1/account/web-session-handoff with the desktop access token (no body),
|
||||
// refreshing once on 401 through the shared policy. On 200 writes the single-use `token_hash`;
|
||||
// otherwise writes the server's error code (e.g. WEB_SESSION_HANDOFF_UNAVAILABLE) into
|
||||
// `error_code`. Blocking: call from a worker thread. Never logs the token.
|
||||
int request_web_session_handoff(std::string& token_hash, std::string& error_code, unsigned int& http_code);
|
||||
|
||||
int connect_server() override;
|
||||
bool is_server_connected() override;
|
||||
int refresh_connection() override;
|
||||
|
||||
Reference in New Issue
Block a user