KiCad PCB EDA Suite
Loading...
Searching...
No Matches
panel_remote_symbol.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include "panel_remote_symbol.h"
22
23#include <bitmaps.h>
24#include <build_version.h>
25#include <common.h>
27#include <eeschema_settings.h>
28#include <kiplatform/webview.h>
29#include <ki_exception.h>
30#include <lib_symbol.h>
33#include <pgm_base.h>
34#include <project_sch.h>
36#include <sch_edit_frame.h>
37#include <string_utils.h>
39#include <tool/tool_manager.h>
40#include <tools/sch_actions.h>
43#include <oauth/oauth_pkce.h>
44
45#ifndef wxUSE_BASE64
46#define wxUSE_BASE64 1
47#endif
48#include <wx/base64.h>
49
50#include <wx/choice.h>
51#include <wx/datetime.h>
52#include <wx/dir.h>
53#include <wx/ffile.h>
54#include <wx/filefn.h>
55#include <wx/intl.h>
56#include <wx/log.h>
57#include <wx/mstream.h>
58#include <wx/settings.h>
59#include <wx/sizer.h>
60#include <wx/stdpaths.h>
61#include <wx/webview.h>
62
63#include <algorithm>
64
65#include <nlohmann/json.hpp>
66#include <zstd.h>
67
68
69bool PANEL_REMOTE_SYMBOL::decodeBase64Payload( const std::string& aEncoded,
70 std::vector<uint8_t>& aOutput,
71 wxString& aError ) const
72{
73 if( aEncoded.empty() )
74 {
75 aError = _( "Missing payload data." );
76 return false;
77 }
78
79 wxMemoryBuffer buffer = wxBase64Decode( wxString::FromUTF8( aEncoded.c_str() ) );
80
81 if( buffer.IsEmpty() )
82 {
83 aError = _( "Failed to decode base64 payload." );
84 return false;
85 }
86
87 aOutput.resize( buffer.GetDataLen() );
88 memcpy( aOutput.data(), buffer.GetData(), buffer.GetDataLen() );
89 return true;
90}
91
92
93bool PANEL_REMOTE_SYMBOL::decompressIfNeeded( const std::string& aCompression,
94 const std::vector<uint8_t>& aInput,
95 std::vector<uint8_t>& aOutput,
96 wxString& aError ) const
97{
98 if( aCompression.empty() || aCompression == "NONE" )
99 {
100 aOutput = aInput;
101 return true;
102 }
103
104 if( aCompression != "ZSTD" )
105 {
106 aError = wxString::Format( _( "Unsupported compression '%s'." ),
107 wxString::FromUTF8( aCompression ) );
108 return false;
109 }
110
111 if( aInput.empty() )
112 {
113 aError = _( "Compressed payload was empty." );
114 return false;
115 }
116
117 unsigned long long expectedSize = ZSTD_getFrameContentSize( aInput.data(), aInput.size() );
118
119 if( expectedSize == ZSTD_CONTENTSIZE_ERROR || expectedSize == ZSTD_CONTENTSIZE_UNKNOWN )
120 expectedSize = static_cast<unsigned long long>( aInput.size() ) * 4;
121
122 static constexpr unsigned long long FALLBACK_MAX = 64ULL * 1024 * 1024;
123
124 unsigned long long maxBytes = ( m_hasSelectedProviderMetadata
125 && m_selectedProviderMetadata.max_download_bytes > 0 )
126 ? static_cast<unsigned long long>(
127 m_selectedProviderMetadata.max_download_bytes )
128 : FALLBACK_MAX;
129
130 if( expectedSize > maxBytes )
131 {
132 aError = wxString::Format( _( "Decompressed size %llu exceeds limit %llu." ),
133 expectedSize, maxBytes );
134 return false;
135 }
136
137 aOutput.resize( expectedSize );
138
139 size_t decompressed = ZSTD_decompress( aOutput.data(), expectedSize, aInput.data(), aInput.size() );
140
141 if( ZSTD_isError( decompressed ) )
142 {
143 aError = wxString::Format( _( "ZSTD decompression failed: %s" ),
144 wxString::FromUTF8( ZSTD_getErrorName( decompressed ) ) );
145 return false;
146 }
147
148 aOutput.resize( decompressed );
149 return true;
150}
151
152
153wxString PANEL_REMOTE_SYMBOL::sanitizeForScript( const std::string& aJson ) const
154{
155 wxString script = wxString::FromUTF8( aJson.c_str() );
156 script.Replace( "\\", "\\\\" );
157 script.Replace( "'", "\\'" );
158 return script;
159}
160
161
163 wxPanel( aParent ),
164 m_frame( aParent ),
165 m_dataSourceChoice( nullptr ),
166 m_configButton( nullptr ),
167 m_refreshButton( nullptr ),
168 m_webView( nullptr ),
169 m_selectedProviderIndex( wxNOT_FOUND ),
172 m_pendingHandshake( false )
173{
174 wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL );
175 wxBoxSizer* controlsSizer = new wxBoxSizer( wxHORIZONTAL );
176
177 m_dataSourceChoice = new wxChoice( this, wxID_ANY );
178 m_dataSourceChoice->SetMinSize( FromDIP( wxSize( 180, -1 ) ) );
179 controlsSizer->Add( m_dataSourceChoice, 1, wxEXPAND | wxRIGHT, FromDIP( 4 ) );
180
181 m_refreshButton = new BITMAP_BUTTON( this, wxID_ANY );
183 m_refreshButton->SetToolTip( _( "Reload current provider page" ) );
184 controlsSizer->Add( m_refreshButton, 0, wxRIGHT, FromDIP( 2 ) );
185
186 m_configButton = new BITMAP_BUTTON( this, wxID_ANY );
188 m_configButton->SetToolTip( _( "Configure remote providers" ) );
189 controlsSizer->Add( m_configButton, 0, wxALIGN_CENTER_VERTICAL );
190
191 topSizer->Add( controlsSizer, 0, wxEXPAND | wxALL, FromDIP( 4 ) );
192 SetSizer( topSizer );
193
195 m_configButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onConfigure, this );
196 m_refreshButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onRefresh, this );
197 Bind( EVT_OAUTH_LOOPBACK_RESULT, &PANEL_REMOTE_SYMBOL::onOAuthLoopback, this );
198 Bind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
199
201}
202
203
205{
206 SaveCookies();
207 Unbind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
208}
209
210
216
217
219{
220 if( m_webView )
221 return;
222
223 m_webView = new WEBVIEW_PANEL( this );
224 m_webView->AddMessageHandler( wxS( "kicad" ),
225 [this]( const wxString& aPayload )
226 {
227 onKicadMessage( aPayload );
228 } );
229 m_webView->SetHandleExternalLinks( true );
230 m_webView->BindLoadedEvent();
231
232 if( wxWebView* browser = m_webView->GetWebView() )
233 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
234
235 GetSizer()->Add( m_webView, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP( 2 ) );
236 Layout();
237}
238
239
241{
242 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
243 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
244}
245
246
247wxFileName PANEL_REMOTE_SYMBOL::cookieFilePath( const wxString& aProviderId ) const
248{
249 wxFileName cookieFile( wxStandardPaths::Get().GetUserDataDir(), wxEmptyString );
250 cookieFile.AppendDir( wxS( "remote-provider-cookies" ) );
251 cookieFile.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
252 cookieFile.SetFullName( SanitizeRemoteFileComponent( aProviderId, wxS( "provider" ), true ) + wxS( ".json" ) );
253 return cookieFile;
254}
255
256
258{
259 if( !m_webView || m_selectedProviderIndex == wxNOT_FOUND || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
260 return;
261
262 if( wxWebView* browser = m_webView->GetWebView() )
263 {
264 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
265 KIPLATFORM::WEBVIEW::SaveCookies( browser, cookieFile.GetFullPath() );
266 }
267}
268
269
271{
272 if( !m_webView || m_selectedProviderIndex == wxNOT_FOUND || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
273 return;
274
275 if( wxWebView* browser = m_webView->GetWebView() )
276 {
277 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
278
279 if( cookieFile.FileExists() )
280 KIPLATFORM::WEBVIEW::LoadCookies( browser, cookieFile.GetFullPath() );
281 }
282}
283
284
285void PANEL_REMOTE_SYMBOL::clearCookies( bool aDeleteSavedCookieFile )
286{
287 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
289
290 if( aDeleteSavedCookieFile && m_selectedProviderIndex != wxNOT_FOUND
291 && m_selectedProviderIndex < static_cast<int>( m_providerEntries.size() ) )
292 {
293 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
294
295 if( cookieFile.FileExists() )
296 wxRemoveFile( cookieFile.GetFullPath() );
297 }
298}
299
300
302{
303 SaveCookies();
304
306
307 m_providerEntries.clear();
308 m_dataSourceChoice->Clear();
309 m_selectedProviderIndex = wxNOT_FOUND;
311
312 if( settings )
314
315 for( const REMOTE_PROVIDER_ENTRY& entry : m_providerEntries )
316 {
317 wxString label = entry.display_name_override.IsEmpty() ? entry.metadata_url : entry.display_name_override;
318 m_dataSourceChoice->Append( label );
319 }
320
321 if( m_providerEntries.empty() )
322 {
323 m_dataSourceChoice->Enable( false );
324
325 if( m_webView )
326 showMessage( _( "No remote providers configured." ) );
327
328 return;
329 }
330
331 m_dataSourceChoice->Enable( true );
332
333 int selected = 0;
334
335 if( settings && !settings->m_RemoteSymbol.last_used_provider_id.IsEmpty() )
336 {
337 for( size_t ii = 0; ii < m_providerEntries.size(); ++ii )
338 {
339 if( m_providerEntries[ii].provider_id == settings->m_RemoteSymbol.last_used_provider_id )
340 {
341 selected = static_cast<int>( ii );
342 break;
343 }
344 }
345 }
346
347 m_dataSourceChoice->SetSelection( selected );
348
349 if( m_webView )
350 loadProvider( selected );
351}
352
353
354void PANEL_REMOTE_SYMBOL::onDataSourceChanged( wxCommandEvent& aEvent )
355{
356 loadProvider( aEvent.GetSelection() );
357}
358
359
360void PANEL_REMOTE_SYMBOL::onConfigure( wxCommandEvent& aEvent )
361{
362 wxUnusedVar( aEvent );
363
364 DIALOG_REMOTE_SYMBOL_CONFIG dlg( this );
365 dlg.ShowModal();
367}
368
369
370void PANEL_REMOTE_SYMBOL::onRefresh( wxCommandEvent& aEvent )
371{
372 wxUnusedVar( aEvent );
373
374 if( m_selectedProviderIndex != wxNOT_FOUND )
376}
377
378
379void PANEL_REMOTE_SYMBOL::onDarkModeToggle( wxSysColourChangedEvent& aEvent )
380{
381 aEvent.Skip();
382
383 if( m_selectedProviderIndex != wxNOT_FOUND )
385}
386
387
389{
390 if( aIndex < 0 || aIndex >= static_cast<int>( m_providerEntries.size() ) )
391 return false;
392
393 SaveCookies();
394 clearCookies( false );
395
398
399 if( !m_providerClient.DiscoverProvider( m_providerEntries[aIndex].metadata_url, metadata, error ) )
400 {
401 showMessage( error.message.IsEmpty() ? _( "Unable to load remote provider metadata." ) : error.message );
402 return false;
403 }
404
408 m_pendingHandshake = true;
409
410 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
411 settings->m_RemoteSymbol.last_used_provider_id = m_providerEntries[aIndex].provider_id;
412
413 LoadCookies();
414 loadProviderPage( metadata, loadAccessToken( m_providerEntries[aIndex] ) );
415 return true;
416}
417
418
420 const wxString& aAccessToken )
421{
422 if( !m_webView )
423 return;
424
425 m_pendingHandshake = true;
426
427 if( !aAccessToken.IsEmpty() && !aMetadata.session_bootstrap_url.IsEmpty() )
428 {
429 bootstrapAuthenticatedSession( aMetadata, aAccessToken );
430 return;
431 }
432
433 m_webView->LoadURL( aMetadata.panel_url );
434}
435
436
438 const wxString& aAccessToken )
439{
440 wxString nonceUrl;
442
443 if( !m_providerClient.ExchangeBootstrapNonce( aMetadata, aAccessToken, nonceUrl, error ) )
444 {
445 wxLogWarning( "Session bootstrap nonce exchange failed: %s", error.message );
446 m_webView->LoadURL( aMetadata.panel_url );
447 return;
448 }
449
450 m_webView->LoadURL( nonceUrl );
451}
452
453
454void PANEL_REMOTE_SYMBOL::showMessage( const wxString& aMessage )
455{
456 if( !m_webView )
457 return;
458
459 wxColour bgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW );
460 wxColour fgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOWTEXT );
461
462 wxString html;
463 html << wxS( "<html><head><style>" )
464 << wxString::Format( wxS( "body { background-color: #%02x%02x%02x; color: #%02x%02x%02x; "
465 "font-family: system-ui, sans-serif; padding: 10px; }" ),
466 bgColour.Red(), bgColour.Green(), bgColour.Blue(),
467 fgColour.Red(), fgColour.Green(), fgColour.Blue() )
468 << wxS( "</style></head><body><p>" ) << EscapeHTML( aMessage )
469 << wxS( "</p></body></html>" );
470
471 m_webView->SetPage( html );
472}
473
474
475void PANEL_REMOTE_SYMBOL::onWebViewLoaded( wxWebViewEvent& aEvent )
476{
478 {
479 const wxString url = aEvent.GetURL();
480
481 if( !url.StartsWith( wxS( "file://" ) ) )
482 {
483 CallAfter( [this]()
484 {
485 m_pendingHandshake = false;
487 } );
488 }
489 }
490
491 aEvent.Skip();
492}
493
494
496{
497 if( !m_webView )
498 return;
499
500 m_sessionId = KIID();
502
503 nlohmann::json params = nlohmann::json::object();
504 params["client_name"] = "KiCad";
505 params["client_version"] = GetSemanticVersion().ToStdString();
506 params["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
507 sendRpcNotification( wxS( "NEW_SESSION" ), std::move( params ) );
508}
509
510
511void PANEL_REMOTE_SYMBOL::sendRpcEnvelope( nlohmann::json aEnvelope )
512{
513 if( !m_webView )
514 return;
515
516 const wxString script = wxString::Format( wxS( "window.kiclient.postMessage('%s');" ),
517 sanitizeForScript( aEnvelope.dump() ) );
518 m_webView->RunScriptAsync( script );
519}
520
521
522void PANEL_REMOTE_SYMBOL::sendRpcReply( const wxString& aCommand, int aResponseTo,
523 nlohmann::json aParameters )
524{
525 nlohmann::json envelope = nlohmann::json::object();
526 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
527 envelope["session_id"] = m_sessionId.AsStdString();
528 envelope["message_id"] = ++m_messageIdCounter;
529 envelope["command"] = aCommand.ToStdString();
530 envelope["status"] = "OK";
531 envelope["response_to"] = aResponseTo;
532
533 if( !aParameters.is_null() && !aParameters.empty() )
534 envelope["parameters"] = std::move( aParameters );
535
536 sendRpcEnvelope( std::move( envelope ) );
537}
538
539
540void PANEL_REMOTE_SYMBOL::sendRpcError( const wxString& aCommand, int aResponseTo,
541 const wxString& aErrorCode, const wxString& aErrorMessage )
542{
543 nlohmann::json envelope = nlohmann::json::object();
544 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
545 envelope["session_id"] = m_sessionId.AsStdString();
546 envelope["message_id"] = ++m_messageIdCounter;
547 envelope["command"] = aCommand.ToStdString();
548 envelope["status"] = "ERROR";
549 envelope["response_to"] = aResponseTo;
550 envelope["error_code"] = aErrorCode.ToStdString();
551 envelope["error_message"] = aErrorMessage.ToStdString();
552 sendRpcEnvelope( std::move( envelope ) );
553}
554
555
556void PANEL_REMOTE_SYMBOL::sendRpcNotification( const wxString& aCommand, nlohmann::json aParameters )
557{
558 nlohmann::json envelope = nlohmann::json::object();
559 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
560 envelope["session_id"] = m_sessionId.AsStdString();
561 envelope["message_id"] = ++m_messageIdCounter;
562 envelope["command"] = aCommand.ToStdString();
563 envelope["status"] = "OK";
564
565 if( !aParameters.is_null() && !aParameters.empty() )
566 envelope["parameters"] = std::move( aParameters );
567
568 sendRpcEnvelope( std::move( envelope ) );
569}
570
571
572void PANEL_REMOTE_SYMBOL::onKicadMessage( const wxString& aMessage )
573{
574 wxScopedCharBuffer utf8 = aMessage.ToUTF8();
575
576 if( !utf8 || utf8.length() == 0 )
577 return;
578
579 try
580 {
581 handleRpcMessage( nlohmann::json::parse( utf8.data() ) );
582 }
583 catch( const std::exception& e )
584 {
585 wxLogWarning( "Remote symbol RPC parse error: %s", e.what() );
586 }
587}
588
589
590std::optional<OAUTH_TOKEN_SET> PANEL_REMOTE_SYMBOL::loadTokens( const REMOTE_PROVIDER_ENTRY& aProvider ) const
591{
592 return m_tokenStore.LoadTokens( aProvider.provider_id, wxS( "default" ) );
593}
594
595
597{
598 std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider );
599
600 if( !tokens )
601 return wxString();
602
603 const long long now = static_cast<long long>( wxDateTime::Now().GetTicks() );
604
605 if( tokens->expires_at == 0 || tokens->expires_at > now + 60 )
606 return tokens->access_token;
607
608 if( tokens->refresh_token.IsEmpty() || !m_hasSelectedProviderMetadata )
609 return wxString();
610
613
614 if( !m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
615 return wxString();
616
617 OAUTH_TOKEN_SET refreshed;
618
619 if( !m_providerClient.RefreshAccessToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
620 tokens->refresh_token, refreshed, error ) )
621 {
622 return wxString();
623 }
624
625 if( refreshed.refresh_token.IsEmpty() )
626 refreshed.refresh_token = tokens->refresh_token;
627
628 if( !m_tokenStore.StoreTokens( aProvider.provider_id, wxS( "default" ), refreshed ) )
629 return wxString();
630
631 return refreshed.access_token;
632}
633
634
636 const REMOTE_PROVIDER_METADATA& aMetadata,
637 wxString& aError )
638{
639 aError.clear();
640
642 return true;
643
645 {
646 aError = _( "A remote provider sign-in flow is already in progress." );
647 return false;
648 }
649
651
652 if( !m_providerClient.FetchOAuthServerMetadata( aMetadata, m_pendingOAuthMetadata, error ) )
653 {
654 aError = error.message;
655 return false;
656 }
657
659 m_pendingOAuthSession.authorization_endpoint = m_pendingOAuthMetadata.authorization_endpoint;
660 m_pendingOAuthSession.client_id = aMetadata.auth.client_id;
661
662 wxArrayString scopes;
663
664 for( const wxString& scope : aMetadata.auth.scopes )
665 scopes.Add( scope );
666
667 m_pendingOAuthSession.scope = wxJoin( scopes, ' ' );
671 m_oauthLoopbackServer = std::make_unique<OAUTH_LOOPBACK_SERVER>(
672 this, wxS( "/oauth/callback" ), m_pendingOAuthSession.state );
673
674 if( !m_oauthLoopbackServer->Start() )
675 {
676 m_oauthLoopbackServer.reset();
677 m_pendingProviderId.clear();
678 aError = _( "Unable to start the local OAuth callback listener." );
679 return false;
680 }
681
682 m_pendingOAuthSession.redirect_uri = m_oauthLoopbackServer->GetRedirectUri();
683
684 if( !wxLaunchDefaultBrowser( m_pendingOAuthSession.BuildAuthorizationUrl(), wxBROWSER_NEW_WINDOW ) )
685 {
686 m_oauthLoopbackServer.reset();
687 m_pendingProviderId.clear();
688 aError = _( "Unable to open the system browser for sign-in." );
689 return false;
690 }
691
692 return true;
693}
694
695
696bool PANEL_REMOTE_SYMBOL::signOutProvider( const REMOTE_PROVIDER_ENTRY& aProvider, wxString& aError )
697{
698 aError.clear();
699
700 if( std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider ); tokens )
701 {
704
706 && m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
707 {
708 const wxString tokenToRevoke = !tokens->refresh_token.IsEmpty() ? tokens->refresh_token
709 : tokens->access_token;
710 REMOTE_PROVIDER_ERROR revokeError;
711 m_providerClient.RevokeToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
712 tokenToRevoke, revokeError );
713 }
714 }
715
716 if( !m_tokenStore.DeleteTokens( aProvider.provider_id, wxS( "default" ) ) )
717 {
718 aError = _( "Failed to delete stored remote provider tokens." );
719 return false;
720 }
721
722 clearCookies();
723
724 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
725 {
726 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( aProvider.provider_id ) )
727 {
728 provider->last_account_label.clear();
729 provider->last_auth_status = wxS( "signed_out" );
730 }
731 }
732
733 return true;
734}
735
736
737void PANEL_REMOTE_SYMBOL::handleRpcMessage( const nlohmann::json& aMessage )
738{
739 if( !aMessage.is_object() )
740 return;
741
742 const wxString command = RemoteProviderJsonString( aMessage, "command" );
743
744 if( command.IsEmpty() )
745 return;
746
747 auto messageIdIt = aMessage.find( "message_id" );
748
749 if( messageIdIt == aMessage.end() || !messageIdIt->is_number_integer() )
750 return;
751
752 const int messageId = messageIdIt->get<int>();
753 const int version = aMessage.value( "version", 0 );
754
755 if( version != REMOTE_SYMBOL_SESSION_VERSION )
756 {
757 sendRpcError( command, messageId, wxS( "UNSUPPORTED_VERSION" ),
758 wxString::Format( _( "Unsupported RPC version %d." ), version ) );
759 return;
760 }
761
762 const wxString sessionId = RemoteProviderJsonString( aMessage, "session_id" );
763
764 if( sessionId.IsEmpty() )
765 {
766 sendRpcError( command, messageId, wxS( "INVALID_PARAMETERS" ),
767 _( "Missing session identifier." ) );
768 return;
769 }
770
771 if( !sessionId.IsSameAs( m_sessionId.AsString() ) )
772 {
773 sendRpcError( command, messageId, wxS( "SESSION_MISMATCH" ),
774 _( "Session identifier did not match the active provider session." ) );
775 return;
776 }
777
778 nlohmann::json params = nlohmann::json::object();
779 auto paramsIt = aMessage.find( "parameters" );
780
781 if( paramsIt != aMessage.end() && paramsIt->is_object() )
782 params = *paramsIt;
783
784 const std::string data = aMessage.value( "data", std::string() );
785
786 if( command == wxS( "NEW_SESSION" ) )
787 {
788 nlohmann::json reply = nlohmann::json::object();
789 reply["client_name"] = "KiCad";
790 reply["client_version"] = GetSemanticVersion().ToStdString();
791 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
792 sendRpcReply( command, messageId, std::move( reply ) );
793 return;
794 }
795
796 if( command == wxS( "GET_KICAD_VERSION" ) )
797 {
798 nlohmann::json reply = nlohmann::json::object();
799 reply["kicad_version"] = GetSemanticVersion().ToStdString();
800 sendRpcReply( command, messageId, std::move( reply ) );
801 return;
802 }
803
804 if( command == wxS( "LIST_SUPPORTED_VERSIONS" ) )
805 {
806 nlohmann::json reply = nlohmann::json::object();
807 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
808 sendRpcReply( command, messageId, std::move( reply ) );
809 return;
810 }
811
812 if( command == wxS( "CAPABILITIES" ) )
813 {
814 nlohmann::json reply = nlohmann::json::object();
815 reply["commands"] = { "NEW_SESSION", "GET_KICAD_VERSION", "LIST_SUPPORTED_VERSIONS",
816 "CAPABILITIES", "GET_SOURCE_INFO", "REMOTE_LOGIN", "DL_SYMBOL",
817 "DL_COMPONENT", "DL_FOOTPRINT", "DL_SPICE", "DL_3DMODEL",
818 "PLACE_COMPONENT" };
819 reply["compression"] = { "NONE", "ZSTD" };
820 reply["max_message_size"] = 0;
821 sendRpcReply( command, messageId, std::move( reply ) );
822 return;
823 }
824
825 if( command == wxS( "GET_SOURCE_INFO" ) )
826 {
827 nlohmann::json reply = nlohmann::json::object();
828 reply["provider_id"] = m_selectedProviderIndex == wxNOT_FOUND
829 ? std::string()
830 : m_providerEntries[m_selectedProviderIndex].provider_id.ToStdString();
831 reply["provider_name"] = m_hasSelectedProviderMetadata
832 ? m_selectedProviderMetadata.provider_name.ToStdString()
833 : std::string();
834 reply["panel_url"] = m_hasSelectedProviderMetadata
835 ? m_selectedProviderMetadata.panel_url.ToStdString()
836 : std::string();
837
838 bool authenticated = false;
839
840 if( m_selectedProviderIndex != wxNOT_FOUND )
841 authenticated = !loadAccessToken( m_providerEntries[m_selectedProviderIndex] ).IsEmpty();
842
843 reply["authenticated"] = authenticated;
844 reply["auth_type"] = m_hasSelectedProviderMetadata
846 ? "oauth2"
847 : "none" )
848 : "none";
849 reply["supports_direct_downloads"] =
851 reply["supports_inline_payloads"] =
853 sendRpcReply( command, messageId, std::move( reply ) );
854 return;
855 }
856
857 if( command == wxS( "REMOTE_LOGIN" ) )
858 {
860 {
861 sendRpcError( command, messageId, wxS( "NO_PROVIDER" ),
862 _( "No remote provider is currently selected." ) );
863 return;
864 }
865
867 const bool signOut = params.value( "sign_out", false );
868 const bool interactive = params.value( "interactive", true );
869
870 if( signOut )
871 {
872 wxString error;
873
874 if( !signOutProvider( provider, error ) )
875 {
876 sendRpcError( command, messageId, wxS( "SIGN_OUT_FAILED" ), error );
877 return;
878 }
879
881
882 nlohmann::json reply = nlohmann::json::object();
883 reply["authenticated"] = false;
884 reply["signed_out"] = true;
885 sendRpcReply( command, messageId, std::move( reply ) );
886 return;
887 }
888
889 const wxString accessToken = loadAccessToken( provider );
890
891 if( !accessToken.IsEmpty() )
892 {
893 nlohmann::json reply = nlohmann::json::object();
894 reply["authenticated"] = true;
895 reply["provider_id"] = provider.provider_id.ToStdString();
896 sendRpcReply( command, messageId, std::move( reply ) );
897 return;
898 }
899
901 {
902 nlohmann::json reply = nlohmann::json::object();
903 reply["authenticated"] = false;
904 reply["auth_type"] = "none";
905 sendRpcReply( command, messageId, std::move( reply ) );
906 return;
907 }
908
909 if( !interactive )
910 {
911 nlohmann::json reply = nlohmann::json::object();
912 reply["authenticated"] = false;
913 reply["started"] = false;
914 sendRpcReply( command, messageId, std::move( reply ) );
915 return;
916 }
917
918 wxString error;
919
920 if( !startInteractiveLogin( provider, m_selectedProviderMetadata, error ) )
921 {
922 sendRpcError( command, messageId, wxS( "LOGIN_FAILED" ), error );
923 return;
924 }
925
926 nlohmann::json reply = nlohmann::json::object();
927 reply["authenticated"] = false;
928 reply["started"] = true;
929 sendRpcReply( command, messageId, std::move( reply ) );
930 return;
931 }
932
933 if( command == wxS( "PLACE_COMPONENT" ) || command == wxS( "DL_COMPONENT" ) || command == wxS( "DL_SYMBOL" )
934 || command == wxS( "DL_FOOTPRINT" ) || command == wxS( "DL_3DMODEL" ) || command == wxS( "DL_SPICE" ) )
935 {
936 const bool placeSymbol = command == wxS( "PLACE_COMPONENT" )
937 || RemoteProviderJsonString( params, "mode" ).IsSameAs( wxS( "PLACE" ), false );
938 const bool isComponent = command == wxS( "DL_COMPONENT" ) || command == wxS( "PLACE_COMPONENT" );
939 wxString error;
940 bool ok = false;
941
942 if( isComponent && params.contains( "assets" ) && params["assets"].is_array() )
943 {
944 ok = receiveComponentManifest( params, placeSymbol, error );
945 }
946 else
947 {
948 const wxString compression = RemoteProviderJsonString( params, "compression" );
949 std::vector<uint8_t> decoded;
950
951 if( !decodeBase64Payload( data, decoded, error ) )
952 {
953 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
954 return;
955 }
956
957 std::vector<uint8_t> payload;
958 const std::string compressionStr =
959 compression.IsEmpty() ? std::string() : std::string( compression.ToUTF8().data() );
960
961 if( !decompressIfNeeded( compressionStr, decoded, payload, error ) )
962 {
963 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
964 return;
965 }
966
967 if( isComponent )
968 ok = receiveComponent( params, payload, placeSymbol, error );
969 else if( command == wxS( "DL_SYMBOL" ) )
970 ok = receiveSymbol( params, payload, error );
971 else if( command == wxS( "DL_FOOTPRINT" ) )
972 ok = receiveFootprint( params, payload, error );
973 else if( command == wxS( "DL_3DMODEL" ) )
974 ok = receive3DModel( params, payload, error );
975 else if( command == wxS( "DL_SPICE" ) )
976 ok = receiveSPICEModel( params, payload, error );
977 }
978
979 if( ok )
980 sendRpcReply( command, messageId );
981 else
982 sendRpcError( command, messageId, wxS( "IMPORT_FAILED" ),
983 error.IsEmpty() ? _( "Unable to process provider payload." ) : error );
984
985 return;
986 }
987
988 sendRpcError( command, messageId, wxS( "UNKNOWN_COMMAND" ),
989 wxString::Format( _( "Command '%s' is not supported." ), command ) );
990}
991
992
993bool PANEL_REMOTE_SYMBOL::receiveSymbol( const nlohmann::json& aParams,
994 const std::vector<uint8_t>& aPayload,
995 wxString& aError,
996 const std::vector<LIB_ID>& aFootprintLinks )
997{
998 const wxString mode = RemoteProviderJsonString( aParams, "mode" );
999 const bool placeAfterDownload = mode.IsSameAs( wxS( "PLACE" ), false );
1000
1001 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) && !placeAfterDownload )
1002 {
1003 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1004 return false;
1005 }
1006
1007 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_SYMBOL_V1" ), false ) )
1008 {
1009 aError = _( "Unsupported symbol payload type." );
1010 return false;
1011 }
1012
1013 if( !m_frame )
1014 {
1015 aError = _( "No schematic editor is available to store symbols." );
1016 return false;
1017 }
1018
1019 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1020
1021 if( !settings )
1022 {
1023 aError = _( "Unable to load schematic settings." );
1024 return false;
1025 }
1026
1027 wxFileName baseDir;
1028
1029 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1030 return false;
1031
1032 wxFileName symDir = baseDir;
1033 symDir.AppendDir( wxS( "symbols" ) );
1034 symDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1035
1036 wxString libItemName = RemoteProviderJsonString( aParams, "name" );
1037
1038 if( libItemName.IsEmpty() )
1039 libItemName = wxS( "symbol" );
1040
1041 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1042
1043 if( libraryName.IsEmpty() )
1044 libraryName = wxS( "symbols" );
1045
1046 const wxString nickname = RemoteLibraryPrefix() + wxS( "_" )
1047 + SanitizeRemoteFileComponent( libraryName, wxS( "symbols" ), true );
1048
1049 wxFileName outFile( symDir );
1050 outFile.SetFullName( nickname + wxS( ".kicad_sym" ) );
1051
1053 settings->m_RemoteSymbol.add_to_global_table, true, aError ) )
1054 return false;
1055
1057
1058 if( !adapter )
1059 {
1060 aError = _( "Unable to access the symbol library manager." );
1061 return false;
1062 }
1063
1064 std::unique_ptr<LIB_SYMBOL> downloadedSymbol =
1065 LoadRemoteSymbolFromPayload( aPayload, libItemName, aError );
1066
1067 if( !downloadedSymbol )
1068 return false;
1069
1070 downloadedSymbol->SetName( libItemName );
1071 LIB_ID savedId;
1072 savedId.SetLibNickname( nickname );
1073 savedId.SetLibItemName( libItemName );
1074 downloadedSymbol->SetLibId( savedId );
1075
1076 ApplyFootprintLinks( *downloadedSymbol, aFootprintLinks );
1077
1078 if( adapter->SaveSymbol( nickname, downloadedSymbol.get(), true ) != SYMBOL_LIBRARY_ADAPTER::SAVE_OK )
1079 {
1080 aError = _( "Unable to save the downloaded symbol." );
1081 return false;
1082 }
1083
1084 // Ownership transferred to the library cache on successful save
1085 (void) downloadedSymbol.release();
1086
1090
1091 // Reload the library entry to pick up the new file, then force a full load so the
1092 // library reaches LOADED state. Without the LoadOne call the library stays in LOADING
1093 // state and GetLibSymbol / the symbol chooser cannot see it.
1095 adapter->LoadOne( nickname );
1096
1097 if( placeAfterDownload )
1098 return PlaceRemoteDownloadedSymbol( m_frame, nickname, libItemName, aError );
1099
1100 return true;
1101}
1102
1103
1104bool PANEL_REMOTE_SYMBOL::receiveComponent( const nlohmann::json& aParams,
1105 const std::vector<uint8_t>& aPayload,
1106 bool aPlaceSymbol, wxString& aError )
1107{
1108 nlohmann::json components;
1109
1110 try
1111 {
1112 components = nlohmann::json::parse( aPayload.begin(), aPayload.end() );
1113 }
1114 catch( const std::exception& e )
1115 {
1116 aError = wxString::Format( _( "Failed to parse component list: %s" ), e.what() );
1117 return false;
1118 }
1119
1120 if( !components.is_array() || components.empty() )
1121 {
1122 aError = _( "Component list must be a non-empty array." );
1123 return false;
1124 }
1125
1126 const wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1127
1128 struct PreparedEntry
1129 {
1130 std::string type;
1131 nlohmann::json params;
1132 std::vector<uint8_t> content;
1133 };
1134
1135 std::vector<PreparedEntry> footprints;
1136 std::vector<PreparedEntry> others; // 3D models, SPICE
1137 std::vector<PreparedEntry> symbols;
1138
1139 for( const nlohmann::json& entry : components )
1140 {
1141 if( !entry.is_object() )
1142 {
1143 aError = _( "Component entries must be objects." );
1144 return false;
1145 }
1146
1147 std::string entryType = entry.value( "type", "" );
1148
1149 if( entryType.empty() )
1150 {
1151 aError = _( "Component entry was missing a type." );
1152 return false;
1153 }
1154
1155 std::transform( entryType.begin(), entryType.end(), entryType.begin(),
1156 []( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
1157
1158 std::vector<uint8_t> content;
1159
1160 if( !decodeBase64Payload( entry.value( "content", std::string() ), content, aError ) )
1161 return false;
1162
1163 const std::string compression = entry.value( "compression", std::string() );
1164
1165 if( !compression.empty() && compression != "NONE" )
1166 {
1167 std::vector<uint8_t> decoded = content;
1168
1169 if( !decompressIfNeeded( compression, decoded, content, aError ) )
1170 return false;
1171 }
1172
1173 wxString entryName = wxString::FromUTF8( entry.value( "name", "" ) );
1174 nlohmann::json entryParams = nlohmann::json::object();
1175
1176 if( !libraryName.IsEmpty() )
1177 entryParams["library"] = libraryName.ToStdString();
1178
1179 if( !entryName.IsEmpty() )
1180 entryParams["name"] = entryName.ToStdString();
1181
1182 if( entryType == "symbol" )
1183 {
1184 entryParams["content_type"] = "KICAD_SYMBOL_V1";
1185 entryParams["mode"] = aPlaceSymbol ? "PLACE" : "SAVE";
1186 symbols.push_back( { std::move( entryType ), std::move( entryParams ),
1187 std::move( content ) } );
1188 }
1189 else if( entryType == "footprint" )
1190 {
1191 entryParams["content_type"] = "KICAD_FOOTPRINT_V1";
1192 entryParams["mode"] = "SAVE";
1193 footprints.push_back( { std::move( entryType ), std::move( entryParams ),
1194 std::move( content ) } );
1195 }
1196 else if( entryType == "3dmodel" )
1197 {
1198 entryParams["content_type"] = "KICAD_3D_MODEL_STEP";
1199 entryParams["mode"] = "SAVE";
1200 others.push_back( { std::move( entryType ), std::move( entryParams ),
1201 std::move( content ) } );
1202 }
1203 else if( entryType == "spice" )
1204 {
1205 entryParams["content_type"] = "KICAD_SPICE_MODEL_V1";
1206 entryParams["mode"] = "SAVE";
1207 others.push_back( { std::move( entryType ), std::move( entryParams ),
1208 std::move( content ) } );
1209 }
1210 else
1211 {
1212 aError = wxString::Format( _( "Unsupported component type '%s'." ),
1213 wxString::FromUTF8( entryType.c_str() ) );
1214 return false;
1215 }
1216 }
1217
1218 // Process footprints first so that any subsequent symbol's Footprint field references
1219 // a LIB_ID that already resolves on disk and in the footprint library table.
1220 std::vector<LIB_ID> footprintLinks;
1221
1222 for( const PreparedEntry& entry : footprints )
1223 {
1224 LIB_ID resolvedId;
1225
1226 if( !receiveFootprint( entry.params, entry.content, aError, &resolvedId ) )
1227 return false;
1228
1229 footprintLinks.push_back( resolvedId );
1230 }
1231
1232 for( const PreparedEntry& entry : others )
1233 {
1234 bool ok = false;
1235
1236 if( entry.type == "3dmodel" )
1237 ok = receive3DModel( entry.params, entry.content, aError );
1238 else
1239 ok = receiveSPICEModel( entry.params, entry.content, aError );
1240
1241 if( !ok )
1242 return false;
1243 }
1244
1245 for( const PreparedEntry& entry : symbols )
1246 {
1247 if( !receiveSymbol( entry.params, entry.content, aError, footprintLinks ) )
1248 return false;
1249 }
1250
1251 return true;
1252}
1253
1254
1255bool PANEL_REMOTE_SYMBOL::receiveFootprint( const nlohmann::json& aParams,
1256 const std::vector<uint8_t>& aPayload,
1257 wxString& aError, LIB_ID* aOutLibId )
1258{
1259 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_FOOTPRINT_V1" ), false ) )
1260 {
1261 aError = _( "Unsupported footprint payload type." );
1262 return false;
1263 }
1264
1265 wxFileName baseDir;
1266
1267 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1268 return false;
1269
1270 wxFileName fpRoot = baseDir;
1271 fpRoot.AppendDir( wxS( "footprints" ) );
1272 fpRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1273
1274 const wxString rawName = RemoteProviderJsonString( aParams, "name" );
1275 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1276
1277 if( libraryName.IsEmpty() )
1278 libraryName = wxS( "footprints" );
1279
1280 // The LIB_ID's item name is the footprint name without the .kicad_mod suffix.
1281 // Build it via the same helper used elsewhere so the local LIB_ID we expose to
1282 // callers always matches the nickname under which the footprint is registered.
1283 const LIB_ID resolvedId = BuildRemoteLibId( libraryName, rawName );
1284 const wxString libNickname = resolvedId.GetUniStringLibNickname();
1285 const wxString fpItemName = resolvedId.GetUniStringLibItemName();
1286
1287 wxFileName libDir = fpRoot;
1288 libDir.AppendDir( libNickname + wxS( ".pretty" ) );
1289 libDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1290
1291 wxString fileName = fpItemName;
1292
1293 if( !fileName.Lower().EndsWith( wxS( ".kicad_mod" ) ) )
1294 fileName += wxS( ".kicad_mod" );
1295
1296 wxFileName outFile( libDir );
1297 outFile.SetFullName( fileName );
1298
1299 if( !WriteRemoteBinaryFile( outFile, aPayload, aError ) )
1300 return false;
1301
1302 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1303
1304 if( !settings )
1305 {
1306 aError = _( "Unable to load schematic settings." );
1307 return false;
1308 }
1309
1311 settings->m_RemoteSymbol.add_to_global_table, true, aError ) )
1312 return false;
1313
1317
1319 libMgr.ReloadLibraryEntry( LIBRARY_TABLE_TYPE::FOOTPRINT, libNickname, scope );
1320 libMgr.LoadLibraryEntry( LIBRARY_TABLE_TYPE::FOOTPRINT, libNickname );
1321
1322 if( aOutLibId )
1323 *aOutLibId = resolvedId;
1324
1325 return true;
1326}
1327
1328
1329bool PANEL_REMOTE_SYMBOL::receive3DModel( const nlohmann::json& aParams,
1330 const std::vector<uint8_t>& aPayload,
1331 wxString& aError )
1332{
1333 wxFileName baseDir;
1334
1335 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1336 return false;
1337
1338 wxFileName modelDir = baseDir;
1339 modelDir.AppendDir( RemoteLibraryPrefix() + wxS( "_3d" ) );
1340 modelDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1341
1342 wxString fileName = RemoteProviderJsonString( aParams, "name" );
1343
1344 if( fileName.IsEmpty() )
1345 fileName = RemoteLibraryPrefix() + wxS( "_model.step" );
1346
1347 fileName = SanitizeRemoteFileComponent( fileName, RemoteLibraryPrefix() + wxS( "_model.step" ) );
1348
1349 wxFileName outFile( modelDir );
1350 outFile.SetFullName( fileName );
1351 return WriteRemoteBinaryFile( outFile, aPayload, aError );
1352}
1353
1354
1355bool PANEL_REMOTE_SYMBOL::receiveSPICEModel( const nlohmann::json& aParams,
1356 const std::vector<uint8_t>& aPayload,
1357 wxString& aError )
1358{
1359 wxFileName baseDir;
1360
1361 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1362 return false;
1363
1364 wxFileName spiceDir = baseDir;
1365 spiceDir.AppendDir( RemoteLibraryPrefix() + wxS( "_spice" ) );
1366 spiceDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1367
1368 wxString fileName = RemoteProviderJsonString( aParams, "name" );
1369
1370 if( fileName.IsEmpty() )
1371 fileName = RemoteLibraryPrefix() + wxS( "_model.cir" );
1372
1373 fileName = SanitizeRemoteFileComponent( fileName, RemoteLibraryPrefix() + wxS( "_model.cir" ) );
1374
1375 if( !fileName.Lower().EndsWith( wxS( ".cir" ) ) )
1376 fileName += wxS( ".cir" );
1377
1378 wxFileName outFile( spiceDir );
1379 outFile.SetFullName( fileName );
1380 return WriteRemoteBinaryFile( outFile, aPayload, aError );
1381}
1382
1383
1384bool PANEL_REMOTE_SYMBOL::receiveComponentManifest( const nlohmann::json& aParams,
1385 bool aPlaceSymbol, wxString& aError )
1386{
1388 {
1389 aError = _( "Remote provider metadata is not loaded." );
1390 return false;
1391 }
1392
1394 manifest.part_id = RemoteProviderJsonString( aParams, "part_id" );
1395 manifest.display_name = RemoteProviderJsonString( aParams, "display_name" );
1396 manifest.summary = RemoteProviderJsonString( aParams, "summary" );
1397 manifest.license = RemoteProviderJsonString( aParams, "license" );
1398
1399 for( const nlohmann::json& assetJson : aParams.at( "assets" ) )
1400 {
1402 asset.asset_type = RemoteProviderJsonString( assetJson, "asset_type" );
1403 asset.name = RemoteProviderJsonString( assetJson, "name" );
1404 asset.target_library = RemoteProviderJsonString( assetJson, "target_library" );
1405 asset.target_name = RemoteProviderJsonString( assetJson, "target_name" );
1406 asset.content_type = RemoteProviderJsonString( assetJson, "content_type" );
1407 asset.download_url = RemoteProviderJsonString( assetJson, "download_url" );
1408 asset.sha256 = RemoteProviderJsonString( assetJson, "sha256" );
1409 asset.required = assetJson.value( "required", false );
1410
1411 auto sizeIt = assetJson.find( "size_bytes" );
1412
1413 if( sizeIt != assetJson.end() && sizeIt->is_number_integer() )
1414 asset.size_bytes = sizeIt->get<long long>();
1415
1416 if( asset.asset_type.IsEmpty() || asset.content_type.IsEmpty() || asset.download_url.IsEmpty()
1417 || asset.size_bytes <= 0 )
1418 {
1419 aError = _( "Manifest assets require asset_type, content_type, size_bytes, and download_url." );
1420 return false;
1421 }
1422
1423 manifest.assets.push_back( asset );
1424 }
1425
1427 context.symbol_name = RemoteProviderJsonString( aParams, "symbol_name" );
1428 context.library_name = RemoteProviderJsonString( aParams, "library_name" );
1429
1430 if( context.symbol_name.IsEmpty() )
1431 {
1432 for( const REMOTE_PROVIDER_PART_ASSET& asset : manifest.assets )
1433 {
1434 if( asset.asset_type == wxS( "symbol" ) )
1435 {
1436 context.symbol_name = asset.target_name;
1437 context.library_name = asset.target_library;
1438 break;
1439 }
1440 }
1441 }
1442
1444 return job.Import( m_selectedProviderMetadata, context, manifest, aPlaceSymbol, aError );
1445}
1446
1447
1448void PANEL_REMOTE_SYMBOL::onOAuthLoopback( wxCommandEvent& aEvent )
1449{
1450 m_oauthLoopbackServer.reset();
1451
1452 if( !aEvent.GetInt() || m_pendingProviderId.IsEmpty() || !m_hasSelectedProviderMetadata )
1453 {
1454 m_pendingProviderId.clear();
1455 showMessage( _( "Remote provider sign-in was cancelled or failed." ) );
1456 return;
1457 }
1458
1459 OAUTH_TOKEN_SET tokens;
1461
1462 if( !m_providerClient.ExchangeAuthorizationCode( m_pendingOAuthMetadata, m_pendingOAuthSession,
1463 aEvent.GetString(), tokens, error ) )
1464 {
1465 m_pendingProviderId.clear();
1466 showMessage( error.message.IsEmpty() ? _( "Remote provider sign-in failed." ) : error.message );
1467 return;
1468 }
1469
1470 if( tokens.refresh_token.IsEmpty() )
1471 {
1472 if( std::optional<OAUTH_TOKEN_SET> existing = m_tokenStore.LoadTokens( m_pendingProviderId, wxS( "default" ) );
1473 existing )
1474 {
1475 tokens.refresh_token = existing->refresh_token;
1476 }
1477 }
1478
1479 if( !m_tokenStore.StoreTokens( m_pendingProviderId, wxS( "default" ), tokens ) )
1480 {
1481 m_pendingProviderId.clear();
1482 showMessage( _( "Failed to store remote provider tokens securely." ) );
1483 return;
1484 }
1485
1486 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
1487 {
1488 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( m_pendingProviderId ) )
1489 {
1490 provider->last_account_label = wxS( "default" );
1491 provider->last_auth_status = wxS( "signed_in" );
1492 }
1493 }
1494
1495 m_pendingProviderId.clear();
1496
1497 if( m_selectedProviderIndex != wxNOT_FOUND )
1499}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:110
wxString GetSemanticVersion()
Get the semantic version string for KiCad defined inside the KiCadVersion.cmake file in the variable ...
A bitmap button widget that behaves like an AUI toolbar item's button when it is drawn.
int ShowModal() override
REMOTE_PROVIDER_SETTINGS m_RemoteSymbol
Definition kiid.h:48
void ReloadLibraryEntry(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH)
std::optional< LIB_STATUS > LoadLibraryEntry(LIBRARY_TABLE_TYPE aType, const wxString &aNickname)
Synchronously loads the named library to LOADED state for the given type.
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:49
int SetLibItemName(const UTF8 &aLibItemName)
Override the library item name portion of the LIB_ID to aLibItemName.
Definition lib_id.cpp:111
int SetLibNickname(const UTF8 &aLibNickname)
Override the logical library name portion of the LIB_ID to aLibNickname.
Definition lib_id.cpp:100
const wxString GetUniStringLibItemName() const
Get strings for display messages in dialogs.
Definition lib_id.h:112
const wxString GetUniStringLibNickname() const
Definition lib_id.h:88
static wxString GenerateCodeVerifier()
static wxString GenerateState()
void onWebViewLoaded(wxWebViewEvent &aEvent)
void onKicadMessage(const wxString &aMessage)
void showMessage(const wxString &aMessage)
void handleRpcMessage(const nlohmann::json &aMessage)
wxFileName cookieFilePath(const wxString &aProviderId) const
void loadProviderPage(const REMOTE_PROVIDER_METADATA &aMetadata, const wxString &aAccessToken)
void sendRpcError(const wxString &aCommand, int aResponseTo, const wxString &aErrorCode, const wxString &aErrorMessage)
bool loadProvider(int aIndex)
bool signOutProvider(const REMOTE_PROVIDER_ENTRY &aProvider, wxString &aError)
bool receiveFootprint(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError, LIB_ID *aOutLibId=nullptr)
OAUTH_SESSION m_pendingOAuthSession
bool receiveComponentManifest(const nlohmann::json &aParams, bool aPlaceSymbol, wxString &aError)
void sendRpcEnvelope(nlohmann::json aEnvelope)
PANEL_REMOTE_SYMBOL(SCH_EDIT_FRAME *aParent)
bool decompressIfNeeded(const std::string &aCompression, const std::vector< uint8_t > &aInput, std::vector< uint8_t > &aOutput, wxString &aError) const
REMOTE_PROVIDER_METADATA m_selectedProviderMetadata
bool decodeBase64Payload(const std::string &aMessage, std::vector< uint8_t > &aOutPayload, wxString &aError) const
void clearCookies(bool aDeleteSavedCookieFile=true)
wxString sanitizeForScript(const std::string &aJson) const
bool receiveSymbol(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError, const std::vector< LIB_ID > &aFootprintLinks={})
void onOAuthLoopback(wxCommandEvent &aEvent)
void onConfigure(wxCommandEvent &aEvent)
bool receiveComponent(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, bool aPlaceSymbol, wxString &aError)
void sendRpcReply(const wxString &aCommand, int aResponseTo, nlohmann::json aParameters=nlohmann::json::object())
bool startInteractiveLogin(const REMOTE_PROVIDER_ENTRY &aProvider, const REMOTE_PROVIDER_METADATA &aMetadata, wxString &aError)
std::vector< REMOTE_PROVIDER_ENTRY > m_providerEntries
wxString loadAccessToken(const REMOTE_PROVIDER_ENTRY &aProvider)
void sendRpcNotification(const wxString &aCommand, nlohmann::json aParameters=nlohmann::json::object())
bool receiveSPICEModel(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
BITMAP_BUTTON * m_configButton
void onDarkModeToggle(wxSysColourChangedEvent &aEvent)
REMOTE_PROVIDER_CLIENT m_providerClient
SECURE_TOKEN_STORE m_tokenStore
bool receive3DModel(const nlohmann::json &aParams, const std::vector< uint8_t > &aPayload, wxString &aError)
BITMAP_BUTTON * m_refreshButton
void onRefresh(wxCommandEvent &aEvent)
void bootstrapAuthenticatedSession(const REMOTE_PROVIDER_METADATA &aMetadata, const wxString &aAccessToken)
REMOTE_PROVIDER_OAUTH_SERVER_METADATA m_pendingOAuthMetadata
std::unique_ptr< OAUTH_LOOPBACK_SERVER > m_oauthLoopbackServer
std::optional< OAUTH_TOKEN_SET > loadTokens(const REMOTE_PROVIDER_ENTRY &aProvider) const
void onDataSourceChanged(wxCommandEvent &aEvent)
virtual LIBRARY_MANAGER & GetLibraryManager() const
Definition pgm_base.h:132
static SYMBOL_LIBRARY_ADAPTER * SymbolLibAdapter(PROJECT *aProject)
Accessor for project symbol library manager adapter.
bool Import(const REMOTE_PROVIDER_METADATA &aProvider, const REMOTE_SYMBOL_IMPORT_CONTEXT &aContext, const REMOTE_PROVIDER_PART_MANIFEST &aManifest, bool aPlaceSymbol, wxString &aError)
Schematic editor (Eeschema) main window.
An interface to the global shared library manager that is schematic-specific and linked to one projec...
std::optional< LIB_STATUS > LoadOne(LIB_DATA *aLib) override
Loads or reloads the given library, if it exists.
SAVE_T SaveSymbol(const wxString &aNickname, const LIB_SYMBOL *aSymbol, bool aOverwrite=true)
Write aSymbol to an existing library given by aNickname.
The common library.
#define _(s)
LIBRARY_TABLE_SCOPE
bool SaveCookies(wxWebView *aWebView, const wxString &aTargetFile)
Save cookies from the given WebView to the specified file.
bool LoadCookies(wxWebView *aWebView, const wxString &aSourceFile)
Load cookies from the specified file into the given WebView.
bool DeleteCookies(wxWebView *aWebView)
Delete all cookies from the given WebView.
#define REMOTE_SYMBOL_SESSION_VERSION
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
wxString RemoteProviderJsonString(const nlohmann::json &aObject, const char *aKey)
Extract an optional string value from a JSON object, returning an empty wxString when the key is abse...
void ApplyFootprintLinks(LIB_SYMBOL &aSymbol, const std::vector< LIB_ID > &aLinks)
Apply a list of footprint LIB_IDs to a symbol about to be saved.
std::unique_ptr< LIB_SYMBOL > LoadRemoteSymbolFromPayload(const std::vector< uint8_t > &aPayload, const wxString &aLibItemName, wxString &aError)
Deserialize a remote-downloaded symbol payload into a LIB_SYMBOL.
LIB_ID BuildRemoteLibId(const wxString &aResolvedLibrary, const wxString &aResolvedItemName)
Build the local LIB_ID for a remote-downloaded library item.
bool PlaceRemoteDownloadedSymbol(SCH_EDIT_FRAME *aFrame, const wxString &aNickname, const wxString &aLibItemName, wxString &aError)
Place a symbol from a remote download into the schematic editor.
bool EnsureRemoteLibraryEntry(LIBRARY_TABLE_TYPE aTableType, const wxFileName &aLibraryPath, const wxString &aNickname, bool aGlobalTable, bool aStrict, wxString &aError)
Add or update a library table entry for a remote download library.
bool WriteRemoteBinaryFile(const wxFileName &aOutput, const std::vector< uint8_t > &aPayload, wxString &aError)
Write binary data to a file, creating parent directories as needed.
bool EnsureRemoteDestinationRoot(wxFileName &aOutDir, wxString &aError)
Resolve and create the configured destination root directory for remote symbol downloads.
wxString RemoteLibraryPrefix()
Return the configured (or default) library prefix for remote downloads, sanitized for use as a filena...
wxString SanitizeRemoteFileComponent(const wxString &aValue, const wxString &aDefault, bool aLower)
Replace non-alphanumeric characters (other than _ - .) with underscores.
T * GetAppSettings(const char *aFilename)
wxString EscapeHTML(const wxString &aString)
Return a new wxString escaped for embedding in HTML.
wxString provider_id
REMOTE_PROVIDER_AUTH_METADATA auth
std::vector< REMOTE_PROVIDER_PART_ASSET > assets
std::vector< REMOTE_PROVIDER_ENTRY > providers