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