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 <https://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, const std::vector<uint8_t>& aInput,
94 std::vector<uint8_t>& aOutput, wxString& aError ) const
95{
96 if( aCompression.empty() || aCompression == "NONE" )
97 {
98 aOutput = aInput;
99 return true;
100 }
101
102 if( aCompression != "ZSTD" )
103 {
104 aError = wxString::Format( _( "Unsupported compression '%s'." ), wxString::FromUTF8( aCompression ) );
105 return false;
106 }
107
108 if( aInput.empty() )
109 {
110 aError = _( "Compressed payload was empty." );
111 return false;
112 }
113
114 unsigned long long expectedSize = ZSTD_getFrameContentSize( aInput.data(), aInput.size() );
115
116 if( expectedSize == ZSTD_CONTENTSIZE_ERROR || expectedSize == ZSTD_CONTENTSIZE_UNKNOWN )
117 expectedSize = static_cast<unsigned long long>( aInput.size() ) * 4;
118
119 static constexpr unsigned long long FALLBACK_MAX = 64ULL * 1024 * 1024;
120
121 unsigned long long maxBytes = ( m_hasSelectedProviderMetadata
122 && m_selectedProviderMetadata.max_download_bytes > 0 )
123 ? static_cast<unsigned long long>(
124 m_selectedProviderMetadata.max_download_bytes )
125 : FALLBACK_MAX;
126
127 if( expectedSize > maxBytes )
128 {
129 aError = wxString::Format( _( "Decompressed size %llu exceeds limit %llu." ),
130 expectedSize, maxBytes );
131 return false;
132 }
133
134 aOutput.resize( expectedSize );
135
136 size_t decompressed = ZSTD_decompress( aOutput.data(), expectedSize, aInput.data(), aInput.size() );
137
138 if( ZSTD_isError( decompressed ) )
139 {
140 aError = wxString::Format( _( "ZSTD decompression failed: %s" ),
141 wxString::FromUTF8( ZSTD_getErrorName( decompressed ) ) );
142 return false;
143 }
144
145 aOutput.resize( decompressed );
146 return true;
147}
148
149
150wxString PANEL_REMOTE_SYMBOL::sanitizeForScript( const std::string& aJson ) const
151{
152 wxString script = wxString::FromUTF8( aJson.c_str() );
153 script.Replace( "\\", "\\\\" );
154 script.Replace( "'", "\\'" );
155 return script;
156}
157
158
160 wxPanel( aParent ),
161 m_frame( aParent ),
162 m_dataSourceChoice( nullptr ),
163 m_configButton( nullptr ),
164 m_refreshButton( nullptr ),
165 m_webView( nullptr ),
166 m_selectedProviderIndex( wxNOT_FOUND ),
169 m_pendingHandshake( false )
170{
171 wxBoxSizer* topSizer = new wxBoxSizer( wxVERTICAL );
172 wxBoxSizer* controlsSizer = new wxBoxSizer( wxHORIZONTAL );
173
174 m_dataSourceChoice = new wxChoice( this, wxID_ANY );
175 m_dataSourceChoice->SetMinSize( FromDIP( wxSize( 180, -1 ) ) );
176 controlsSizer->Add( m_dataSourceChoice, 1, wxEXPAND | wxRIGHT, FromDIP( 4 ) );
177
178 m_refreshButton = new BITMAP_BUTTON( this, wxID_ANY );
180 m_refreshButton->SetToolTip( _( "Reload current provider page" ) );
181 controlsSizer->Add( m_refreshButton, 0, wxRIGHT, FromDIP( 2 ) );
182
183 m_configButton = new BITMAP_BUTTON( this, wxID_ANY );
185 m_configButton->SetToolTip( _( "Configure remote providers" ) );
186 controlsSizer->Add( m_configButton, 0, wxALIGN_CENTER_VERTICAL );
187
188 topSizer->Add( controlsSizer, 0, wxEXPAND | wxALL, FromDIP( 4 ) );
189 SetSizer( topSizer );
190
192 m_configButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onConfigure, this );
193 m_refreshButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onRefresh, this );
194 Bind( EVT_OAUTH_LOOPBACK_RESULT, &PANEL_REMOTE_SYMBOL::onOAuthLoopback, this );
195 Bind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
196
198}
199
200
202{
203 SaveCookies();
204 Unbind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
205}
206
207
213
214
216{
217 if( m_webView )
218 return;
219
220 m_webView = new WEBVIEW_PANEL( this );
221 m_webView->AddMessageHandler( wxS( "kicad" ),
222 [this]( const wxString& aPayload )
223 {
224 onKicadMessage( aPayload );
225 } );
226 m_webView->SetHandleExternalLinks( true );
227 m_webView->BindLoadedEvent();
228
229 if( wxWebView* browser = m_webView->GetWebView() )
230 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
231
232 GetSizer()->Add( m_webView, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP( 2 ) );
233 Layout();
234}
235
236
238{
239 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
240 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
241}
242
243
244wxFileName PANEL_REMOTE_SYMBOL::cookieFilePath( const wxString& aProviderId ) const
245{
246 wxFileName cookieFile( wxStandardPaths::Get().GetUserDataDir(), wxEmptyString );
247 cookieFile.AppendDir( wxS( "remote-provider-cookies" ) );
248 cookieFile.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
249 cookieFile.SetFullName( SanitizeRemoteFileComponent( aProviderId, wxS( "provider" ), true ) + wxS( ".json" ) );
250 return cookieFile;
251}
252
253
255{
256 if( !m_webView
257 || m_selectedProviderIndex == wxNOT_FOUND
258 || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
259 {
260 return;
261 }
262
263 if( wxWebView* browser = m_webView->GetWebView() )
264 {
265 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
266 KIPLATFORM::WEBVIEW::SaveCookies( browser, cookieFile.GetFullPath() );
267 }
268}
269
270
272{
273 if( !m_webView
274 || m_selectedProviderIndex == wxNOT_FOUND
275 || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
276 {
277 return;
278 }
279
280 if( wxWebView* browser = m_webView->GetWebView() )
281 {
282 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
283
284 if( cookieFile.FileExists() )
285 KIPLATFORM::WEBVIEW::LoadCookies( browser, cookieFile.GetFullPath() );
286 }
287}
288
289
290void PANEL_REMOTE_SYMBOL::clearCookies( bool aDeleteSavedCookieFile )
291{
292 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
294
295 if( aDeleteSavedCookieFile
296 && m_selectedProviderIndex != wxNOT_FOUND
297 && m_selectedProviderIndex < static_cast<int>( m_providerEntries.size() ) )
298 {
299 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
300
301 if( cookieFile.FileExists() )
302 wxRemoveFile( cookieFile.GetFullPath() );
303 }
304}
305
306
308{
309 SaveCookies();
310
312
313 m_providerEntries.clear();
314 m_dataSourceChoice->Clear();
315 m_selectedProviderIndex = wxNOT_FOUND;
317
318 if( settings )
320
321 for( const REMOTE_PROVIDER_ENTRY& entry : m_providerEntries )
322 {
323 wxString label = entry.display_name_override.IsEmpty() ? entry.metadata_url : entry.display_name_override;
324 m_dataSourceChoice->Append( label );
325 }
326
327 if( m_providerEntries.empty() )
328 {
329 m_dataSourceChoice->Enable( false );
330
331 if( m_webView )
332 showMessage( _( "No remote providers configured." ) );
333
334 return;
335 }
336
337 m_dataSourceChoice->Enable( true );
338
339 int selected = 0;
340
341 if( settings && !settings->m_RemoteSymbol.last_used_provider_id.IsEmpty() )
342 {
343 for( size_t ii = 0; ii < m_providerEntries.size(); ++ii )
344 {
345 if( m_providerEntries[ii].provider_id == settings->m_RemoteSymbol.last_used_provider_id )
346 {
347 selected = static_cast<int>( ii );
348 break;
349 }
350 }
351 }
352
353 m_dataSourceChoice->SetSelection( selected );
354
355 if( m_webView )
356 loadProvider( selected );
357}
358
359
360void PANEL_REMOTE_SYMBOL::onDataSourceChanged( wxCommandEvent& aEvent )
361{
362 loadProvider( aEvent.GetSelection() );
363}
364
365
366void PANEL_REMOTE_SYMBOL::onConfigure( wxCommandEvent& aEvent )
367{
368 wxUnusedVar( aEvent );
369
370 DIALOG_REMOTE_SYMBOL_CONFIG dlg( this );
371 dlg.ShowModal();
373}
374
375
376void PANEL_REMOTE_SYMBOL::onRefresh( wxCommandEvent& aEvent )
377{
378 wxUnusedVar( aEvent );
379
380 if( m_selectedProviderIndex != wxNOT_FOUND )
382}
383
384
385void PANEL_REMOTE_SYMBOL::onDarkModeToggle( wxSysColourChangedEvent& aEvent )
386{
387 aEvent.Skip();
388
389 if( m_selectedProviderIndex != wxNOT_FOUND )
391}
392
393
395{
396 if( aIndex < 0 || aIndex >= static_cast<int>( m_providerEntries.size() ) )
397 return false;
398
399 SaveCookies();
400 clearCookies( false );
401
404
405 if( !m_providerClient.DiscoverProvider( m_providerEntries[aIndex].metadata_url, metadata, error ) )
406 {
407 showMessage( error.message.IsEmpty() ? _( "Unable to load remote provider metadata." ) : error.message );
408 return false;
409 }
410
414 m_pendingHandshake = true;
415
416 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
417 settings->m_RemoteSymbol.last_used_provider_id = m_providerEntries[aIndex].provider_id;
418
419 LoadCookies();
420 loadProviderPage( metadata, loadAccessToken( m_providerEntries[aIndex] ) );
421 return true;
422}
423
424
425void PANEL_REMOTE_SYMBOL::loadProviderPage( const REMOTE_PROVIDER_METADATA& aMetadata, const wxString& aAccessToken )
426{
427 if( !m_webView )
428 return;
429
430 m_pendingHandshake = true;
431
432 if( !aAccessToken.IsEmpty() && !aMetadata.session_bootstrap_url.IsEmpty() )
433 {
434 bootstrapAuthenticatedSession( aMetadata, aAccessToken );
435 return;
436 }
437
438 m_webView->LoadURL( aMetadata.panel_url );
439}
440
441
443 const wxString& aAccessToken )
444{
445 wxString nonceUrl;
447
448 if( !m_providerClient.ExchangeBootstrapNonce( aMetadata, aAccessToken, nonceUrl, error ) )
449 {
450 wxLogWarning( "Session bootstrap nonce exchange failed: %s", error.message );
451 m_webView->LoadURL( aMetadata.panel_url );
452 return;
453 }
454
455 m_webView->LoadURL( nonceUrl );
456}
457
458
459void PANEL_REMOTE_SYMBOL::showMessage( const wxString& aMessage )
460{
461 if( !m_webView )
462 return;
463
464 wxColour bgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW );
465 wxColour fgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOWTEXT );
466
467 wxString html;
468 html << wxS( "<html><head><style>" )
469 << wxString::Format( wxS( "body { background-color: #%02x%02x%02x; color: #%02x%02x%02x; "
470 "font-family: system-ui, sans-serif; padding: 10px; }" ),
471 bgColour.Red(), bgColour.Green(), bgColour.Blue(),
472 fgColour.Red(), fgColour.Green(), fgColour.Blue() )
473 << wxS( "</style></head><body><p>" ) << EscapeHTML( aMessage )
474 << wxS( "</p></body></html>" );
475
476 m_webView->SetPage( html );
477}
478
479
480void PANEL_REMOTE_SYMBOL::onWebViewLoaded( wxWebViewEvent& aEvent )
481{
483 {
484 const wxString url = aEvent.GetURL();
485
486 if( !url.StartsWith( wxS( "file://" ) ) )
487 {
488 CallAfter( [this]()
489 {
490 m_pendingHandshake = false;
492 } );
493 }
494 }
495
496 aEvent.Skip();
497}
498
499
501{
502 if( !m_webView )
503 return;
504
505 m_sessionId = KIID();
507
508 nlohmann::json params = nlohmann::json::object();
509 params["client_name"] = "KiCad";
510 params["client_version"] = GetSemanticVersion().ToStdString();
511 params["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
512 sendRpcNotification( wxS( "NEW_SESSION" ), std::move( params ) );
513}
514
515
516void PANEL_REMOTE_SYMBOL::sendRpcEnvelope( nlohmann::json aEnvelope )
517{
518 if( !m_webView )
519 return;
520
521 const wxString script = wxString::Format( wxS( "window.kiclient.postMessage('%s');" ),
522 sanitizeForScript( aEnvelope.dump() ) );
523 m_webView->RunScriptAsync( script );
524}
525
526
527void PANEL_REMOTE_SYMBOL::sendRpcReply( const wxString& aCommand, int aResponseTo,
528 nlohmann::json aParameters )
529{
530 nlohmann::json envelope = nlohmann::json::object();
531 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
532 envelope["session_id"] = m_sessionId.AsStdString();
533 envelope["message_id"] = ++m_messageIdCounter;
534 envelope["command"] = aCommand.ToStdString();
535 envelope["status"] = "OK";
536 envelope["response_to"] = aResponseTo;
537
538 if( !aParameters.is_null() && !aParameters.empty() )
539 envelope["parameters"] = std::move( aParameters );
540
541 sendRpcEnvelope( std::move( envelope ) );
542}
543
544
545void PANEL_REMOTE_SYMBOL::sendRpcError( const wxString& aCommand, int aResponseTo,
546 const wxString& aErrorCode, const wxString& aErrorMessage )
547{
548 nlohmann::json envelope = nlohmann::json::object();
549 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
550 envelope["session_id"] = m_sessionId.AsStdString();
551 envelope["message_id"] = ++m_messageIdCounter;
552 envelope["command"] = aCommand.ToStdString();
553 envelope["status"] = "ERROR";
554 envelope["response_to"] = aResponseTo;
555 envelope["error_code"] = aErrorCode.ToStdString();
556 envelope["error_message"] = aErrorMessage.ToStdString();
557 sendRpcEnvelope( std::move( envelope ) );
558}
559
560
561void PANEL_REMOTE_SYMBOL::sendRpcNotification( const wxString& aCommand, nlohmann::json aParameters )
562{
563 nlohmann::json envelope = nlohmann::json::object();
564 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
565 envelope["session_id"] = m_sessionId.AsStdString();
566 envelope["message_id"] = ++m_messageIdCounter;
567 envelope["command"] = aCommand.ToStdString();
568 envelope["status"] = "OK";
569
570 if( !aParameters.is_null() && !aParameters.empty() )
571 envelope["parameters"] = std::move( aParameters );
572
573 sendRpcEnvelope( std::move( envelope ) );
574}
575
576
577void PANEL_REMOTE_SYMBOL::onKicadMessage( const wxString& aMessage )
578{
579 wxScopedCharBuffer utf8 = aMessage.ToUTF8();
580
581 if( !utf8 || utf8.length() == 0 )
582 return;
583
584 try
585 {
586 handleRpcMessage( nlohmann::json::parse( utf8.data() ) );
587 }
588 catch( const std::exception& e )
589 {
590 wxLogWarning( "Remote symbol RPC parse error: %s", e.what() );
591 }
592}
593
594
595std::optional<OAUTH_TOKEN_SET> PANEL_REMOTE_SYMBOL::loadTokens( const REMOTE_PROVIDER_ENTRY& aProvider ) const
596{
597 return m_tokenStore.LoadTokens( aProvider.provider_id, wxS( "default" ) );
598}
599
600
602{
603 std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider );
604
605 if( !tokens )
606 return wxString();
607
608 const long long now = static_cast<long long>( wxDateTime::Now().GetTicks() );
609
610 if( tokens->expires_at == 0 || tokens->expires_at > now + 60 )
611 return tokens->access_token;
612
613 if( tokens->refresh_token.IsEmpty() || !m_hasSelectedProviderMetadata )
614 return wxString();
615
618
619 if( !m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
620 return wxString();
621
622 OAUTH_TOKEN_SET refreshed;
623
624 if( !m_providerClient.RefreshAccessToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
625 tokens->refresh_token, refreshed, error ) )
626 {
627 return wxString();
628 }
629
630 if( refreshed.refresh_token.IsEmpty() )
631 refreshed.refresh_token = tokens->refresh_token;
632
633 if( !m_tokenStore.StoreTokens( aProvider.provider_id, wxS( "default" ), refreshed ) )
634 return wxString();
635
636 return refreshed.access_token;
637}
638
639
641 const REMOTE_PROVIDER_METADATA& aMetadata,
642 wxString& aError )
643{
644 aError.clear();
645
647 return true;
648
650 {
651 aError = _( "A remote provider sign-in flow is already in progress." );
652 return false;
653 }
654
656
657 if( !m_providerClient.FetchOAuthServerMetadata( aMetadata, m_pendingOAuthMetadata, error ) )
658 {
659 aError = error.message;
660 return false;
661 }
662
664 m_pendingOAuthSession.authorization_endpoint = m_pendingOAuthMetadata.authorization_endpoint;
665 m_pendingOAuthSession.client_id = aMetadata.auth.client_id;
666
667 wxArrayString scopes;
668
669 for( const wxString& scope : aMetadata.auth.scopes )
670 scopes.Add( scope );
671
672 m_pendingOAuthSession.scope = wxJoin( scopes, ' ' );
676 m_oauthLoopbackServer = std::make_unique<OAUTH_LOOPBACK_SERVER>( this, wxS( "/oauth/callback" ),
677 m_pendingOAuthSession.state );
678
679 if( !m_oauthLoopbackServer->Start() )
680 {
681 m_oauthLoopbackServer.reset();
682 m_pendingProviderId.clear();
683 aError = _( "Unable to start the local OAuth callback listener." );
684 return false;
685 }
686
687 m_pendingOAuthSession.redirect_uri = m_oauthLoopbackServer->GetRedirectUri();
688
689 if( !wxLaunchDefaultBrowser( m_pendingOAuthSession.BuildAuthorizationUrl(), wxBROWSER_NEW_WINDOW ) )
690 {
691 m_oauthLoopbackServer.reset();
692 m_pendingProviderId.clear();
693 aError = _( "Unable to open the system browser for sign-in." );
694 return false;
695 }
696
697 return true;
698}
699
700
701bool PANEL_REMOTE_SYMBOL::signOutProvider( const REMOTE_PROVIDER_ENTRY& aProvider, wxString& aError )
702{
703 aError.clear();
704
705 if( std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider ); tokens )
706 {
709
711 && m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
712 {
713 const wxString tokenToRevoke = !tokens->refresh_token.IsEmpty() ? tokens->refresh_token
714 : tokens->access_token;
715 REMOTE_PROVIDER_ERROR revokeError;
716 m_providerClient.RevokeToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
717 tokenToRevoke, revokeError );
718 }
719 }
720
721 if( !m_tokenStore.DeleteTokens( aProvider.provider_id, wxS( "default" ) ) )
722 {
723 aError = _( "Failed to delete stored remote provider tokens." );
724 return false;
725 }
726
727 clearCookies();
728
729 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
730 {
731 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( aProvider.provider_id ) )
732 {
733 provider->last_account_label.clear();
734 provider->last_auth_status = wxS( "signed_out" );
735 }
736 }
737
738 return true;
739}
740
741
742void PANEL_REMOTE_SYMBOL::handleRpcMessage( const nlohmann::json& aMessage )
743{
744 if( !aMessage.is_object() )
745 return;
746
747 const wxString command = RemoteProviderJsonString( aMessage, "command" );
748
749 if( command.IsEmpty() )
750 return;
751
752 auto messageIdIt = aMessage.find( "message_id" );
753
754 if( messageIdIt == aMessage.end() || !messageIdIt->is_number_integer() )
755 return;
756
757 const int messageId = messageIdIt->get<int>();
758 const int version = aMessage.value( "version", 0 );
759
760 if( version != REMOTE_SYMBOL_SESSION_VERSION )
761 {
762 sendRpcError( command, messageId, wxS( "UNSUPPORTED_VERSION" ),
763 wxString::Format( _( "Unsupported RPC version %d." ), version ) );
764 return;
765 }
766
767 const wxString sessionId = RemoteProviderJsonString( aMessage, "session_id" );
768
769 if( sessionId.IsEmpty() )
770 {
771 sendRpcError( command, messageId, wxS( "INVALID_PARAMETERS" ), _( "Missing session identifier." ) );
772 return;
773 }
774
775 if( !sessionId.IsSameAs( m_sessionId.AsString() ) )
776 {
777 sendRpcError( command, messageId, wxS( "SESSION_MISMATCH" ),
778 _( "Session identifier did not match the active provider session." ) );
779 return;
780 }
781
782 nlohmann::json params = nlohmann::json::object();
783 auto paramsIt = aMessage.find( "parameters" );
784
785 if( paramsIt != aMessage.end() && paramsIt->is_object() )
786 params = *paramsIt;
787
788 const std::string data = aMessage.value( "data", std::string() );
789
790 if( command == wxS( "NEW_SESSION" ) )
791 {
792 nlohmann::json reply = nlohmann::json::object();
793 reply["client_name"] = "KiCad";
794 reply["client_version"] = GetSemanticVersion().ToStdString();
795 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
796 sendRpcReply( command, messageId, std::move( reply ) );
797 return;
798 }
799
800 if( command == wxS( "GET_KICAD_VERSION" ) )
801 {
802 nlohmann::json reply = nlohmann::json::object();
803 reply["kicad_version"] = GetSemanticVersion().ToStdString();
804 sendRpcReply( command, messageId, std::move( reply ) );
805 return;
806 }
807
808 if( command == wxS( "LIST_SUPPORTED_VERSIONS" ) )
809 {
810 nlohmann::json reply = nlohmann::json::object();
811 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
812 sendRpcReply( command, messageId, std::move( reply ) );
813 return;
814 }
815
816 if( command == wxS( "CAPABILITIES" ) )
817 {
818 nlohmann::json reply = nlohmann::json::object();
819 reply["commands"] = { "NEW_SESSION", "GET_KICAD_VERSION", "LIST_SUPPORTED_VERSIONS",
820 "CAPABILITIES", "GET_SOURCE_INFO", "REMOTE_LOGIN", "DL_SYMBOL",
821 "DL_COMPONENT", "DL_FOOTPRINT", "DL_SPICE", "DL_3DMODEL",
822 "PLACE_COMPONENT" };
823 reply["compression"] = { "NONE", "ZSTD" };
824 reply["max_message_size"] = 0;
825 sendRpcReply( command, messageId, std::move( reply ) );
826 return;
827 }
828
829 if( command == wxS( "GET_SOURCE_INFO" ) )
830 {
831 nlohmann::json reply = nlohmann::json::object();
832 reply["provider_id"] = m_selectedProviderIndex == wxNOT_FOUND
833 ? std::string()
834 : m_providerEntries[m_selectedProviderIndex].provider_id.ToStdString();
835 reply["provider_name"] = m_hasSelectedProviderMetadata
836 ? m_selectedProviderMetadata.provider_name.ToStdString()
837 : std::string();
838 reply["panel_url"] = m_hasSelectedProviderMetadata
839 ? m_selectedProviderMetadata.panel_url.ToStdString()
840 : std::string();
841
842 bool authenticated = false;
843
844 if( m_selectedProviderIndex != wxNOT_FOUND )
845 authenticated = !loadAccessToken( m_providerEntries[m_selectedProviderIndex] ).IsEmpty();
846
847 reply["authenticated"] = authenticated;
848 reply["auth_type"] = m_hasSelectedProviderMetadata
850 ? "oauth2"
851 : "none" )
852 : "none";
853 reply["supports_direct_downloads"] =
855 reply["supports_inline_payloads"] =
857 sendRpcReply( command, messageId, std::move( reply ) );
858 return;
859 }
860
861 if( command == wxS( "REMOTE_LOGIN" ) )
862 {
864 {
865 sendRpcError( command, messageId, wxS( "NO_PROVIDER" ), _( "No remote provider is currently selected." ) );
866 return;
867 }
868
870 const bool signOut = params.value( "sign_out", false );
871 const bool interactive = params.value( "interactive", true );
872
873 if( signOut )
874 {
875 wxString error;
876
877 if( !signOutProvider( provider, error ) )
878 {
879 sendRpcError( command, messageId, wxS( "SIGN_OUT_FAILED" ), error );
880 return;
881 }
882
884
885 nlohmann::json reply = nlohmann::json::object();
886 reply["authenticated"] = false;
887 reply["signed_out"] = true;
888 sendRpcReply( command, messageId, std::move( reply ) );
889 return;
890 }
891
892 const wxString accessToken = loadAccessToken( provider );
893
894 if( !accessToken.IsEmpty() )
895 {
896 nlohmann::json reply = nlohmann::json::object();
897 reply["authenticated"] = true;
898 reply["provider_id"] = provider.provider_id.ToStdString();
899 sendRpcReply( command, messageId, std::move( reply ) );
900 return;
901 }
902
904 {
905 nlohmann::json reply = nlohmann::json::object();
906 reply["authenticated"] = false;
907 reply["auth_type"] = "none";
908 sendRpcReply( command, messageId, std::move( reply ) );
909 return;
910 }
911
912 if( !interactive )
913 {
914 nlohmann::json reply = nlohmann::json::object();
915 reply["authenticated"] = false;
916 reply["started"] = false;
917 sendRpcReply( command, messageId, std::move( reply ) );
918 return;
919 }
920
921 wxString error;
922
923 if( !startInteractiveLogin( provider, m_selectedProviderMetadata, error ) )
924 {
925 sendRpcError( command, messageId, wxS( "LOGIN_FAILED" ), error );
926 return;
927 }
928
929 nlohmann::json reply = nlohmann::json::object();
930 reply["authenticated"] = false;
931 reply["started"] = true;
932 sendRpcReply( command, messageId, std::move( reply ) );
933 return;
934 }
935
936 if( command == wxS( "PLACE_COMPONENT" ) || command == wxS( "DL_COMPONENT" ) || command == wxS( "DL_SYMBOL" )
937 || command == wxS( "DL_FOOTPRINT" ) || command == wxS( "DL_3DMODEL" ) || command == wxS( "DL_SPICE" ) )
938 {
939 const bool placeSymbol = command == wxS( "PLACE_COMPONENT" )
940 || RemoteProviderJsonString( params, "mode" ).IsSameAs( wxS( "PLACE" ), false );
941 const bool isComponent = command == wxS( "DL_COMPONENT" ) || command == wxS( "PLACE_COMPONENT" );
942 wxString error;
943 bool ok = false;
944
945 if( isComponent && params.contains( "assets" ) && params["assets"].is_array() )
946 {
947 ok = receiveComponentManifest( params, placeSymbol, error );
948 }
949 else
950 {
951 const wxString compression = RemoteProviderJsonString( params, "compression" );
952 std::vector<uint8_t> decoded;
953
954 if( !decodeBase64Payload( data, decoded, error ) )
955 {
956 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
957 return;
958 }
959
960 std::vector<uint8_t> payload;
961 const std::string compressionStr = compression.IsEmpty() ? std::string()
962 : std::string( compression.ToUTF8().data() );
963
964 if( !decompressIfNeeded( compressionStr, decoded, payload, error ) )
965 {
966 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
967 return;
968 }
969
970 if( isComponent )
971 ok = receiveComponent( params, payload, placeSymbol, error );
972 else if( command == wxS( "DL_SYMBOL" ) )
973 ok = receiveSymbol( params, payload, error );
974 else if( command == wxS( "DL_FOOTPRINT" ) )
975 ok = receiveFootprint( params, payload, error );
976 else if( command == wxS( "DL_3DMODEL" ) )
977 ok = receive3DModel( params, payload, error );
978 else if( command == wxS( "DL_SPICE" ) )
979 ok = receiveSPICEModel( params, payload, error );
980 }
981
982 if( ok )
983 {
984 sendRpcReply( command, messageId );
985 }
986 else
987 {
988 sendRpcError( command, messageId, wxS( "IMPORT_FAILED" ),
989 error.IsEmpty() ? _( "Unable to process provider payload." ) : error );
990 }
991
992 return;
993 }
994
995 sendRpcError( command, messageId, wxS( "UNKNOWN_COMMAND" ),
996 wxString::Format( _( "Command '%s' is not supported." ), command ) );
997}
998
999
1000bool PANEL_REMOTE_SYMBOL::receiveSymbol( const nlohmann::json& aParams, const std::vector<uint8_t>& aPayload,
1001 wxString& aError, const std::vector<LIB_ID>& aFootprintLinks )
1002{
1003 const wxString mode = RemoteProviderJsonString( aParams, "mode" );
1004 const bool placeAfterDownload = mode.IsSameAs( wxS( "PLACE" ), false );
1005
1006 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) && !placeAfterDownload )
1007 {
1008 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1009 return false;
1010 }
1011
1012 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_SYMBOL_V1" ), false ) )
1013 {
1014 aError = _( "Unsupported symbol payload type." );
1015 return false;
1016 }
1017
1018 if( !m_frame )
1019 {
1020 aError = _( "No schematic editor is available to store symbols." );
1021 return false;
1022 }
1023
1024 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1025
1026 if( !settings )
1027 {
1028 aError = _( "Unable to load schematic settings." );
1029 return false;
1030 }
1031
1032 wxFileName baseDir;
1033
1034 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1035 return false;
1036
1037 wxFileName symDir = baseDir;
1038 symDir.AppendDir( wxS( "symbols" ) );
1039 symDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1040
1041 wxString libItemName = RemoteProviderJsonString( aParams, "name" );
1042
1043 if( libItemName.IsEmpty() )
1044 libItemName = wxS( "symbol" );
1045
1046 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1047
1048 if( libraryName.IsEmpty() )
1049 libraryName = wxS( "symbols" );
1050
1051 const wxString nickname = RemoteLibraryPrefix()
1052 + wxS( "_" )
1053 + SanitizeRemoteFileComponent( libraryName, wxS( "symbols" ), true );
1054
1055 wxFileName outFile( symDir );
1056 outFile.SetFullName( nickname + wxS( ".kicad_sym" ) );
1057
1059
1060 if( !adapter )
1061 {
1062 aError = _( "Unable to access the symbol library manager." );
1063 return false;
1064 }
1065
1066 std::unique_ptr<LIB_SYMBOL> downloadedSymbol = LoadRemoteSymbolFromPayload( aPayload, libItemName, aError );
1067
1068 if( !downloadedSymbol )
1069 return false;
1070
1071 downloadedSymbol->SetName( libItemName );
1072 LIB_ID savedId;
1073 savedId.SetLibNickname( nickname );
1074 savedId.SetLibItemName( libItemName );
1075 downloadedSymbol->SetLibId( savedId );
1076
1077 ApplyFootprintLinks( *downloadedSymbol, aFootprintLinks );
1078
1079 if( !SaveRemoteSymbolToLibrary( *adapter, outFile, nickname, settings->m_RemoteSymbol.add_to_global_table,
1080 std::move( downloadedSymbol ), aError ) )
1081 {
1082 return false;
1083 }
1084
1085 if( placeAfterDownload )
1086 return PlaceRemoteDownloadedSymbol( m_frame, nickname, libItemName, aError );
1087
1088 return true;
1089}
1090
1091
1092bool PANEL_REMOTE_SYMBOL::receiveComponent( const nlohmann::json& aParams, const std::vector<uint8_t>& aPayload,
1093 bool aPlaceSymbol, wxString& aError )
1094{
1095 nlohmann::json components;
1096
1097 try
1098 {
1099 components = nlohmann::json::parse( aPayload.begin(), aPayload.end() );
1100 }
1101 catch( const std::exception& e )
1102 {
1103 aError = wxString::Format( _( "Failed to parse component list: %s" ), e.what() );
1104 return false;
1105 }
1106
1107 if( !components.is_array() || components.empty() )
1108 {
1109 aError = _( "Component list must be a non-empty array." );
1110 return false;
1111 }
1112
1113 const wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1114
1115 struct PreparedEntry
1116 {
1117 std::string type;
1118 nlohmann::json params;
1119 std::vector<uint8_t> content;
1120 };
1121
1122 std::vector<PreparedEntry> footprints;
1123 std::vector<PreparedEntry> others; // 3D models, SPICE
1124 std::vector<PreparedEntry> symbols;
1125
1126 for( const nlohmann::json& entry : components )
1127 {
1128 if( !entry.is_object() )
1129 {
1130 aError = _( "Component entries must be objects." );
1131 return false;
1132 }
1133
1134 std::string entryType = entry.value( "type", "" );
1135
1136 if( entryType.empty() )
1137 {
1138 aError = _( "Component entry was missing a type." );
1139 return false;
1140 }
1141
1142 std::transform( entryType.begin(), entryType.end(), entryType.begin(),
1143 []( unsigned char c )
1144 {
1145 return static_cast<char>( std::tolower( c ) );
1146 } );
1147
1148 std::vector<uint8_t> content;
1149
1150 if( !decodeBase64Payload( entry.value( "content", std::string() ), content, aError ) )
1151 return false;
1152
1153 const std::string compression = entry.value( "compression", std::string() );
1154
1155 if( !compression.empty() && compression != "NONE" )
1156 {
1157 std::vector<uint8_t> decoded = content;
1158
1159 if( !decompressIfNeeded( compression, decoded, content, aError ) )
1160 return false;
1161 }
1162
1163 wxString entryName = wxString::FromUTF8( entry.value( "name", "" ) );
1164 nlohmann::json entryParams = nlohmann::json::object();
1165
1166 if( !libraryName.IsEmpty() )
1167 entryParams["library"] = libraryName.ToStdString();
1168
1169 if( !entryName.IsEmpty() )
1170 entryParams["name"] = entryName.ToStdString();
1171
1172 if( entryType == "symbol" )
1173 {
1174 entryParams["content_type"] = "KICAD_SYMBOL_V1";
1175 entryParams["mode"] = aPlaceSymbol ? "PLACE" : "SAVE";
1176 symbols.push_back( { std::move( entryType ), std::move( entryParams ), std::move( content ) } );
1177 }
1178 else if( entryType == "footprint" )
1179 {
1180 entryParams["content_type"] = "KICAD_FOOTPRINT_V1";
1181 entryParams["mode"] = "SAVE";
1182 footprints.push_back( { std::move( entryType ), std::move( entryParams ), std::move( content ) } );
1183 }
1184 else if( entryType == "3dmodel" )
1185 {
1186 entryParams["content_type"] = "KICAD_3D_MODEL_STEP";
1187 entryParams["mode"] = "SAVE";
1188 others.push_back( { std::move( entryType ), std::move( entryParams ), std::move( content ) } );
1189 }
1190 else if( entryType == "spice" )
1191 {
1192 entryParams["content_type"] = "KICAD_SPICE_MODEL_V1";
1193 entryParams["mode"] = "SAVE";
1194 others.push_back( { std::move( entryType ), std::move( entryParams ), std::move( content ) } );
1195 }
1196 else
1197 {
1198 aError = wxString::Format( _( "Unsupported component type '%s'." ),
1199 wxString::FromUTF8( entryType.c_str() ) );
1200 return false;
1201 }
1202 }
1203
1204 // Process footprints first so that any subsequent symbol's Footprint field references
1205 // a LIB_ID that already resolves on disk and in the footprint library table.
1206 std::vector<LIB_ID> footprintLinks;
1207
1208 for( const PreparedEntry& entry : footprints )
1209 {
1210 LIB_ID resolvedId;
1211
1212 if( !receiveFootprint( entry.params, entry.content, aError, &resolvedId ) )
1213 return false;
1214
1215 footprintLinks.push_back( resolvedId );
1216 }
1217
1218 for( const PreparedEntry& entry : others )
1219 {
1220 bool ok = false;
1221
1222 if( entry.type == "3dmodel" )
1223 ok = receive3DModel( entry.params, entry.content, aError );
1224 else
1225 ok = receiveSPICEModel( entry.params, entry.content, aError );
1226
1227 if( !ok )
1228 return false;
1229 }
1230
1231 for( const PreparedEntry& entry : symbols )
1232 {
1233 if( !receiveSymbol( entry.params, entry.content, aError, footprintLinks ) )
1234 return false;
1235 }
1236
1237 return true;
1238}
1239
1240
1241bool PANEL_REMOTE_SYMBOL::receiveFootprint( const nlohmann::json& aParams, const std::vector<uint8_t>& aPayload,
1242 wxString& aError, LIB_ID* aOutLibId )
1243{
1244 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_FOOTPRINT_V1" ), false ) )
1245 {
1246 aError = _( "Unsupported footprint payload type." );
1247 return false;
1248 }
1249
1250 wxFileName baseDir;
1251
1252 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1253 return false;
1254
1255 wxFileName fpRoot = baseDir;
1256 fpRoot.AppendDir( wxS( "footprints" ) );
1257 fpRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1258
1259 const wxString rawName = RemoteProviderJsonString( aParams, "name" );
1260 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1261
1262 if( libraryName.IsEmpty() )
1263 libraryName = wxS( "footprints" );
1264
1265 // The LIB_ID's item name is the footprint name without the .kicad_mod suffix.
1266 // Build it via the same helper used elsewhere so the local LIB_ID we expose to
1267 // callers always matches the nickname under which the footprint is registered.
1268 const LIB_ID resolvedId = BuildRemoteLibId( libraryName, rawName );
1269 const wxString libNickname = resolvedId.GetUniStringLibNickname();
1270 const wxString fpItemName = resolvedId.GetUniStringLibItemName();
1271
1272 wxFileName libDir = fpRoot;
1273 libDir.AppendDir( libNickname + wxS( ".pretty" ) );
1274 libDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1275
1276 wxString fileName = fpItemName + wxS( ".kicad_mod" );
1277
1278 wxFileName outFile( libDir );
1279 outFile.SetFullName( fileName );
1280
1281 if( !WriteRemoteBinaryFile( outFile, aPayload, aError ) )
1282 return false;
1283
1284 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1285
1286 if( !settings )
1287 {
1288 aError = _( "Unable to load schematic settings." );
1289 return false;
1290 }
1291
1293 settings->m_RemoteSymbol.add_to_global_table, true, aError ) )
1294 {
1295 return false;
1296 }
1297
1300
1302 libMgr.ReloadLibraryEntry( LIBRARY_TABLE_TYPE::FOOTPRINT, libNickname, scope );
1303 libMgr.LoadLibraryEntry( LIBRARY_TABLE_TYPE::FOOTPRINT, libNickname );
1304
1305 if( aOutLibId )
1306 *aOutLibId = resolvedId;
1307
1308 return true;
1309}
1310
1311
1312bool PANEL_REMOTE_SYMBOL::receive3DModel( const nlohmann::json& aParams, const std::vector<uint8_t>& aPayload,
1313 wxString& aError )
1314{
1315 wxFileName baseDir;
1316
1317 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1318 return false;
1319
1320 wxFileName modelDir = baseDir;
1321 modelDir.AppendDir( RemoteLibraryPrefix() + wxS( "_3d" ) );
1322 modelDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1323
1324 wxString fileName = RemoteProviderJsonString( aParams, "name" );
1325
1326 if( fileName.IsEmpty() )
1327 fileName = RemoteLibraryPrefix() + wxS( "_model.step" );
1328
1329 fileName = SanitizeRemoteFileComponent( fileName, RemoteLibraryPrefix() + wxS( "_model.step" ) );
1330
1331 wxFileName outFile( modelDir );
1332 outFile.SetFullName( fileName );
1333 return WriteRemoteBinaryFile( outFile, aPayload, aError );
1334}
1335
1336
1337bool PANEL_REMOTE_SYMBOL::receiveSPICEModel( const nlohmann::json& aParams, const std::vector<uint8_t>& aPayload,
1338 wxString& aError )
1339{
1340 wxFileName baseDir;
1341
1342 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1343 return false;
1344
1345 wxFileName spiceDir = baseDir;
1346 spiceDir.AppendDir( RemoteLibraryPrefix() + wxS( "_spice" ) );
1347 spiceDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1348
1349 wxString fileName = RemoteProviderJsonString( aParams, "name" );
1350
1351 if( fileName.IsEmpty() )
1352 fileName = RemoteLibraryPrefix() + wxS( "_model.cir" );
1353
1354 fileName = SanitizeRemoteFileComponent( fileName, RemoteLibraryPrefix() + wxS( "_model.cir" ) );
1355
1356 if( !fileName.Lower().EndsWith( wxS( ".cir" ) ) )
1357 fileName += wxS( ".cir" );
1358
1359 wxFileName outFile( spiceDir );
1360 outFile.SetFullName( fileName );
1361 return WriteRemoteBinaryFile( outFile, aPayload, aError );
1362}
1363
1364
1365bool PANEL_REMOTE_SYMBOL::receiveComponentManifest( const nlohmann::json& aParams, bool aPlaceSymbol,
1366 wxString& aError )
1367{
1369 {
1370 aError = _( "Remote provider metadata is not loaded." );
1371 return false;
1372 }
1373
1375 manifest.part_id = RemoteProviderJsonString( aParams, "part_id" );
1376 manifest.display_name = RemoteProviderJsonString( aParams, "display_name" );
1377 manifest.summary = RemoteProviderJsonString( aParams, "summary" );
1378 manifest.license = RemoteProviderJsonString( aParams, "license" );
1379
1380 for( const nlohmann::json& assetJson : aParams.at( "assets" ) )
1381 {
1383 asset.asset_type = RemoteProviderJsonString( assetJson, "asset_type" );
1384 asset.name = RemoteProviderJsonString( assetJson, "name" );
1385 asset.target_library = RemoteProviderJsonString( assetJson, "target_library" );
1386 asset.target_name = RemoteProviderJsonString( assetJson, "target_name" );
1387 asset.content_type = RemoteProviderJsonString( assetJson, "content_type" );
1388 asset.download_url = RemoteProviderJsonString( assetJson, "download_url" );
1389 asset.sha256 = RemoteProviderJsonString( assetJson, "sha256" );
1390 asset.required = assetJson.value( "required", false );
1391
1392 auto sizeIt = assetJson.find( "size_bytes" );
1393
1394 if( sizeIt != assetJson.end() && sizeIt->is_number_integer() )
1395 asset.size_bytes = sizeIt->get<long long>();
1396
1397 if( asset.asset_type.IsEmpty() || asset.content_type.IsEmpty() || asset.download_url.IsEmpty()
1398 || asset.size_bytes <= 0 )
1399 {
1400 aError = _( "Manifest assets require asset_type, content_type, size_bytes, and download_url." );
1401 return false;
1402 }
1403
1404 manifest.assets.push_back( asset );
1405 }
1406
1408 context.symbol_name = RemoteProviderJsonString( aParams, "symbol_name" );
1409 context.library_name = RemoteProviderJsonString( aParams, "library_name" );
1410
1411 if( context.symbol_name.IsEmpty() )
1412 {
1413 for( const REMOTE_PROVIDER_PART_ASSET& asset : manifest.assets )
1414 {
1415 if( asset.asset_type == wxS( "symbol" ) )
1416 {
1417 context.symbol_name = asset.target_name;
1418 context.library_name = asset.target_library;
1419 break;
1420 }
1421 }
1422 }
1423
1425 return job.Import( m_selectedProviderMetadata, context, manifest, aPlaceSymbol, aError );
1426}
1427
1428
1429void PANEL_REMOTE_SYMBOL::onOAuthLoopback( wxCommandEvent& aEvent )
1430{
1431 m_oauthLoopbackServer.reset();
1432
1433 if( !aEvent.GetInt() || m_pendingProviderId.IsEmpty() || !m_hasSelectedProviderMetadata )
1434 {
1435 m_pendingProviderId.clear();
1436 showMessage( _( "Remote provider sign-in was cancelled or failed." ) );
1437 return;
1438 }
1439
1440 OAUTH_TOKEN_SET tokens;
1442
1443 if( !m_providerClient.ExchangeAuthorizationCode( m_pendingOAuthMetadata, m_pendingOAuthSession,
1444 aEvent.GetString(), tokens, error ) )
1445 {
1446 m_pendingProviderId.clear();
1447 showMessage( error.message.IsEmpty() ? _( "Remote provider sign-in failed." ) : error.message );
1448 return;
1449 }
1450
1451 if( tokens.refresh_token.IsEmpty() )
1452 {
1453 if( std::optional<OAUTH_TOKEN_SET> existing = m_tokenStore.LoadTokens( m_pendingProviderId, wxS( "default" ) );
1454 existing )
1455 {
1456 tokens.refresh_token = existing->refresh_token;
1457 }
1458 }
1459
1460 if( !m_tokenStore.StoreTokens( m_pendingProviderId, wxS( "default" ), tokens ) )
1461 {
1462 m_pendingProviderId.clear();
1463 showMessage( _( "Failed to store remote provider tokens securely." ) );
1464 return;
1465 }
1466
1467 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
1468 {
1469 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( m_pendingProviderId ) )
1470 {
1471 provider->last_account_label = wxS( "default" );
1472 provider->last_auth_status = wxS( "signed_in" );
1473 }
1474 }
1475
1476 m_pendingProviderId.clear();
1477
1478 if( m_selectedProviderIndex != wxNOT_FOUND )
1480}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
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:46
void ReloadLibraryEntry(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH)
Synchronously recreates the plugin for and reloads the given library.
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:45
int SetLibItemName(const UTF8 &aLibItemName)
Override the library item name portion of the LIB_ID to aLibItemName.
Definition lib_id.cpp:124
int SetLibNickname(const UTF8 &aLibNickname)
Override the logical library name portion of the LIB_ID to aLibNickname.
Definition lib_id.cpp:113
const wxString GetUniStringLibItemName() const
Get strings for display messages in dialogs.
Definition lib_id.h:108
const wxString GetUniStringLibNickname() const
Definition lib_id.h:84
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:125
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...
#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 SaveRemoteSymbolToLibrary(SYMBOL_LIBRARY_ADAPTER &aAdapter, const wxFileName &aLibraryFile, const wxString &aNickname, bool aGlobalTable, std::unique_ptr< LIB_SYMBOL > aSymbol, wxString &aError)
Save a downloaded symbol into the remote symbol library aNickname backed by aLibraryFile.
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
static std::string decoded(const std::string &aBase64)