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.reset( 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
262 m_webView = new WEBVIEW_PANEL( this );
263 m_webView->AddMessageHandler( wxS( "kicad" ),
264 [this]( const wxString& aPayload )
265 {
266 onKicadMessage( aPayload );
267 } );
268 m_webView->SetHandleExternalLinks( true );
269 m_webView->BindLoadedEvent();
270
271 if( wxWebView* browser = m_webView->GetWebView() )
272 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
273
274 topSizer->Add( m_webView, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP( 2 ) );
275
276 SetSizer( topSizer );
277
279 m_configButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onConfigure, this );
280 m_refreshButton->Bind( wxEVT_BUTTON, &PANEL_REMOTE_SYMBOL::onRefresh, this );
281 Bind( EVT_OAUTH_LOOPBACK_RESULT, &PANEL_REMOTE_SYMBOL::onOAuthLoopback, this );
282 Bind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
283
285}
286
287
289{
290 SaveCookies();
291 Unbind( wxEVT_SYS_COLOUR_CHANGED, wxSysColourChangedEventHandler( PANEL_REMOTE_SYMBOL::onDarkModeToggle ), this );
292}
293
294
296{
297 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
298 browser->Bind( wxEVT_WEBVIEW_LOADED, &PANEL_REMOTE_SYMBOL::onWebViewLoaded, this );
299}
300
301
302wxFileName PANEL_REMOTE_SYMBOL::cookieFilePath( const wxString& aProviderId ) const
303{
304 wxFileName cookieFile( wxStandardPaths::Get().GetUserDataDir(), wxEmptyString );
305 cookieFile.AppendDir( wxS( "remote-provider-cookies" ) );
306 cookieFile.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
307 cookieFile.SetFullName( SanitizeRemoteFileComponent( aProviderId, wxS( "provider" ), true ) + wxS( ".json" ) );
308 return cookieFile;
309}
310
311
313{
314 if( !m_webView || m_selectedProviderIndex == wxNOT_FOUND || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
315 return;
316
317 if( wxWebView* browser = m_webView->GetWebView() )
318 {
319 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
320 KIPLATFORM::WEBVIEW::SaveCookies( browser, cookieFile.GetFullPath() );
321 }
322}
323
324
326{
327 if( !m_webView || m_selectedProviderIndex == wxNOT_FOUND || m_selectedProviderIndex >= static_cast<int>( m_providerEntries.size() ) )
328 return;
329
330 if( wxWebView* browser = m_webView->GetWebView() )
331 {
332 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
333
334 if( cookieFile.FileExists() )
335 KIPLATFORM::WEBVIEW::LoadCookies( browser, cookieFile.GetFullPath() );
336 }
337}
338
339
340void PANEL_REMOTE_SYMBOL::clearCookies( bool aDeleteSavedCookieFile )
341{
342 if( wxWebView* browser = m_webView ? m_webView->GetWebView() : nullptr )
344
345 if( aDeleteSavedCookieFile && m_selectedProviderIndex != wxNOT_FOUND
346 && m_selectedProviderIndex < static_cast<int>( m_providerEntries.size() ) )
347 {
348 const wxFileName cookieFile = cookieFilePath( m_providerEntries[m_selectedProviderIndex].provider_id );
349
350 if( cookieFile.FileExists() )
351 wxRemoveFile( cookieFile.GetFullPath() );
352 }
353}
354
355
357{
358 SaveCookies();
359
361
362 m_providerEntries.clear();
363 m_dataSourceChoice->Clear();
364 m_selectedProviderIndex = wxNOT_FOUND;
366
367 if( settings )
369
370 for( const REMOTE_PROVIDER_ENTRY& entry : m_providerEntries )
371 {
372 wxString label = entry.display_name_override.IsEmpty() ? entry.metadata_url : entry.display_name_override;
373 m_dataSourceChoice->Append( label );
374 }
375
376 if( m_providerEntries.empty() )
377 {
378 m_dataSourceChoice->Enable( false );
379 showMessage( _( "No remote providers configured." ) );
380 return;
381 }
382
383 m_dataSourceChoice->Enable( true );
384
385 int selected = 0;
386
387 if( settings && !settings->m_RemoteSymbol.last_used_provider_id.IsEmpty() )
388 {
389 for( size_t ii = 0; ii < m_providerEntries.size(); ++ii )
390 {
391 if( m_providerEntries[ii].provider_id == settings->m_RemoteSymbol.last_used_provider_id )
392 {
393 selected = static_cast<int>( ii );
394 break;
395 }
396 }
397 }
398
399 m_dataSourceChoice->SetSelection( selected );
400 loadProvider( selected );
401}
402
403
404void PANEL_REMOTE_SYMBOL::onDataSourceChanged( wxCommandEvent& aEvent )
405{
406 loadProvider( aEvent.GetSelection() );
407}
408
409
410void PANEL_REMOTE_SYMBOL::onConfigure( wxCommandEvent& aEvent )
411{
412 wxUnusedVar( aEvent );
413
414 DIALOG_REMOTE_SYMBOL_CONFIG dlg( this );
415 dlg.ShowModal();
417}
418
419
420void PANEL_REMOTE_SYMBOL::onRefresh( wxCommandEvent& aEvent )
421{
422 wxUnusedVar( aEvent );
423
424 if( m_selectedProviderIndex != wxNOT_FOUND )
426}
427
428
429void PANEL_REMOTE_SYMBOL::onDarkModeToggle( wxSysColourChangedEvent& aEvent )
430{
431 aEvent.Skip();
432
433 if( m_selectedProviderIndex != wxNOT_FOUND )
435}
436
437
439{
440 if( aIndex < 0 || aIndex >= static_cast<int>( m_providerEntries.size() ) )
441 return false;
442
443 SaveCookies();
444 clearCookies( false );
445
448
449 if( !m_providerClient.DiscoverProvider( m_providerEntries[aIndex].metadata_url, metadata, error ) )
450 {
451 showMessage( error.message.IsEmpty() ? _( "Unable to load remote provider metadata." ) : error.message );
452 return false;
453 }
454
458 m_pendingHandshake = true;
459
460 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
461 settings->m_RemoteSymbol.last_used_provider_id = m_providerEntries[aIndex].provider_id;
462
463 LoadCookies();
464 loadProviderPage( metadata, loadAccessToken( m_providerEntries[aIndex] ) );
465 return true;
466}
467
468
470 const wxString& aAccessToken )
471{
472 if( !m_webView )
473 return;
474
475 m_pendingHandshake = true;
476
477 if( !aAccessToken.IsEmpty() && !aMetadata.session_bootstrap_url.IsEmpty() )
478 {
479 bootstrapAuthenticatedSession( aMetadata, aAccessToken );
480 return;
481 }
482
483 m_webView->LoadURL( aMetadata.panel_url );
484}
485
486
488 const wxString& aAccessToken )
489{
490 wxString nonceUrl;
492
493 if( !m_providerClient.ExchangeBootstrapNonce( aMetadata, aAccessToken, nonceUrl, error ) )
494 {
495 wxLogWarning( "Session bootstrap nonce exchange failed: %s", error.message );
496 m_webView->LoadURL( aMetadata.panel_url );
497 return;
498 }
499
500 m_webView->LoadURL( nonceUrl );
501}
502
503
504void PANEL_REMOTE_SYMBOL::showMessage( const wxString& aMessage )
505{
506 if( !m_webView )
507 return;
508
509 wxColour bgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW );
510 wxColour fgColour = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOWTEXT );
511
512 wxString html;
513 html << wxS( "<html><head><style>" )
514 << wxString::Format( wxS( "body { background-color: #%02x%02x%02x; color: #%02x%02x%02x; "
515 "font-family: system-ui, sans-serif; padding: 10px; }" ),
516 bgColour.Red(), bgColour.Green(), bgColour.Blue(),
517 fgColour.Red(), fgColour.Green(), fgColour.Blue() )
518 << wxS( "</style></head><body><p>" ) << EscapeHTML( aMessage )
519 << wxS( "</p></body></html>" );
520
521 m_webView->SetPage( html );
522}
523
524
525void PANEL_REMOTE_SYMBOL::onWebViewLoaded( wxWebViewEvent& aEvent )
526{
528 {
529 const wxString url = aEvent.GetURL();
530
531 if( !url.StartsWith( wxS( "file://" ) ) )
532 {
533 CallAfter( [this]()
534 {
535 m_pendingHandshake = false;
537 } );
538 }
539 }
540
541 aEvent.Skip();
542}
543
544
546{
547 if( !m_webView )
548 return;
549
550 m_sessionId = KIID();
552
553 nlohmann::json params = nlohmann::json::object();
554 params["client_name"] = "KiCad";
555 params["client_version"] = GetSemanticVersion().ToStdString();
556 params["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
557 sendRpcNotification( wxS( "NEW_SESSION" ), std::move( params ) );
558}
559
560
561void PANEL_REMOTE_SYMBOL::sendRpcEnvelope( nlohmann::json aEnvelope )
562{
563 if( !m_webView )
564 return;
565
566 const wxString script = wxString::Format( wxS( "window.kiclient.postMessage('%s');" ),
567 sanitizeForScript( aEnvelope.dump() ) );
568 m_webView->RunScriptAsync( script );
569}
570
571
572void PANEL_REMOTE_SYMBOL::sendRpcReply( const wxString& aCommand, int aResponseTo,
573 nlohmann::json aParameters )
574{
575 nlohmann::json envelope = nlohmann::json::object();
576 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
577 envelope["session_id"] = m_sessionId.AsStdString();
578 envelope["message_id"] = ++m_messageIdCounter;
579 envelope["command"] = aCommand.ToStdString();
580 envelope["status"] = "OK";
581 envelope["response_to"] = aResponseTo;
582
583 if( !aParameters.is_null() && !aParameters.empty() )
584 envelope["parameters"] = std::move( aParameters );
585
586 sendRpcEnvelope( std::move( envelope ) );
587}
588
589
590void PANEL_REMOTE_SYMBOL::sendRpcError( const wxString& aCommand, int aResponseTo,
591 const wxString& aErrorCode, const wxString& aErrorMessage )
592{
593 nlohmann::json envelope = nlohmann::json::object();
594 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
595 envelope["session_id"] = m_sessionId.AsStdString();
596 envelope["message_id"] = ++m_messageIdCounter;
597 envelope["command"] = aCommand.ToStdString();
598 envelope["status"] = "ERROR";
599 envelope["response_to"] = aResponseTo;
600 envelope["error_code"] = aErrorCode.ToStdString();
601 envelope["error_message"] = aErrorMessage.ToStdString();
602 sendRpcEnvelope( std::move( envelope ) );
603}
604
605
606void PANEL_REMOTE_SYMBOL::sendRpcNotification( const wxString& aCommand, nlohmann::json aParameters )
607{
608 nlohmann::json envelope = nlohmann::json::object();
609 envelope["version"] = REMOTE_SYMBOL_SESSION_VERSION;
610 envelope["session_id"] = m_sessionId.AsStdString();
611 envelope["message_id"] = ++m_messageIdCounter;
612 envelope["command"] = aCommand.ToStdString();
613 envelope["status"] = "OK";
614
615 if( !aParameters.is_null() && !aParameters.empty() )
616 envelope["parameters"] = std::move( aParameters );
617
618 sendRpcEnvelope( std::move( envelope ) );
619}
620
621
622void PANEL_REMOTE_SYMBOL::onKicadMessage( const wxString& aMessage )
623{
624 wxScopedCharBuffer utf8 = aMessage.ToUTF8();
625
626 if( !utf8 || utf8.length() == 0 )
627 return;
628
629 try
630 {
631 handleRpcMessage( nlohmann::json::parse( utf8.data() ) );
632 }
633 catch( const std::exception& e )
634 {
635 wxLogWarning( "Remote symbol RPC parse error: %s", e.what() );
636 }
637}
638
639
640std::optional<OAUTH_TOKEN_SET> PANEL_REMOTE_SYMBOL::loadTokens( const REMOTE_PROVIDER_ENTRY& aProvider ) const
641{
642 return m_tokenStore.LoadTokens( aProvider.provider_id, wxS( "default" ) );
643}
644
645
647{
648 std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider );
649
650 if( !tokens )
651 return wxString();
652
653 const long long now = static_cast<long long>( wxDateTime::Now().GetTicks() );
654
655 if( tokens->expires_at == 0 || tokens->expires_at > now + 60 )
656 return tokens->access_token;
657
658 if( tokens->refresh_token.IsEmpty() || !m_hasSelectedProviderMetadata )
659 return wxString();
660
663
664 if( !m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
665 return wxString();
666
667 OAUTH_TOKEN_SET refreshed;
668
669 if( !m_providerClient.RefreshAccessToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
670 tokens->refresh_token, refreshed, error ) )
671 {
672 return wxString();
673 }
674
675 if( refreshed.refresh_token.IsEmpty() )
676 refreshed.refresh_token = tokens->refresh_token;
677
678 if( !m_tokenStore.StoreTokens( aProvider.provider_id, wxS( "default" ), refreshed ) )
679 return wxString();
680
681 return refreshed.access_token;
682}
683
684
686 const REMOTE_PROVIDER_METADATA& aMetadata,
687 wxString& aError )
688{
689 aError.clear();
690
692 return true;
693
695 {
696 aError = _( "A remote provider sign-in flow is already in progress." );
697 return false;
698 }
699
701
702 if( !m_providerClient.FetchOAuthServerMetadata( aMetadata, m_pendingOAuthMetadata, error ) )
703 {
704 aError = error.message;
705 return false;
706 }
707
709 m_pendingOAuthSession.authorization_endpoint = m_pendingOAuthMetadata.authorization_endpoint;
710 m_pendingOAuthSession.client_id = aMetadata.auth.client_id;
711
712 wxArrayString scopes;
713
714 for( const wxString& scope : aMetadata.auth.scopes )
715 scopes.Add( scope );
716
717 m_pendingOAuthSession.scope = wxJoin( scopes, ' ' );
721 m_oauthLoopbackServer = std::make_unique<OAUTH_LOOPBACK_SERVER>(
722 this, wxS( "/oauth/callback" ), m_pendingOAuthSession.state );
723
724 if( !m_oauthLoopbackServer->Start() )
725 {
726 m_oauthLoopbackServer.reset();
727 m_pendingProviderId.clear();
728 aError = _( "Unable to start the local OAuth callback listener." );
729 return false;
730 }
731
732 m_pendingOAuthSession.redirect_uri = m_oauthLoopbackServer->GetRedirectUri();
733
734 if( !wxLaunchDefaultBrowser( m_pendingOAuthSession.BuildAuthorizationUrl(), wxBROWSER_NEW_WINDOW ) )
735 {
736 m_oauthLoopbackServer.reset();
737 m_pendingProviderId.clear();
738 aError = _( "Unable to open the system browser for sign-in." );
739 return false;
740 }
741
742 return true;
743}
744
745
746bool PANEL_REMOTE_SYMBOL::signOutProvider( const REMOTE_PROVIDER_ENTRY& aProvider, wxString& aError )
747{
748 aError.clear();
749
750 if( std::optional<OAUTH_TOKEN_SET> tokens = loadTokens( aProvider ); tokens )
751 {
754
756 && m_providerClient.FetchOAuthServerMetadata( m_selectedProviderMetadata, oauthMetadata, error ) )
757 {
758 const wxString tokenToRevoke = !tokens->refresh_token.IsEmpty() ? tokens->refresh_token
759 : tokens->access_token;
760 REMOTE_PROVIDER_ERROR revokeError;
761 m_providerClient.RevokeToken( oauthMetadata, m_selectedProviderMetadata.auth.client_id,
762 tokenToRevoke, revokeError );
763 }
764 }
765
766 if( !m_tokenStore.DeleteTokens( aProvider.provider_id, wxS( "default" ) ) )
767 {
768 aError = _( "Failed to delete stored remote provider tokens." );
769 return false;
770 }
771
772 clearCookies();
773
774 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
775 {
776 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( aProvider.provider_id ) )
777 {
778 provider->last_account_label.clear();
779 provider->last_auth_status = wxS( "signed_out" );
780 }
781 }
782
783 return true;
784}
785
786
787void PANEL_REMOTE_SYMBOL::handleRpcMessage( const nlohmann::json& aMessage )
788{
789 if( !aMessage.is_object() )
790 return;
791
792 const wxString command = RemoteProviderJsonString( aMessage, "command" );
793
794 if( command.IsEmpty() )
795 return;
796
797 auto messageIdIt = aMessage.find( "message_id" );
798
799 if( messageIdIt == aMessage.end() || !messageIdIt->is_number_integer() )
800 return;
801
802 const int messageId = messageIdIt->get<int>();
803 const int version = aMessage.value( "version", 0 );
804
805 if( version != REMOTE_SYMBOL_SESSION_VERSION )
806 {
807 sendRpcError( command, messageId, wxS( "UNSUPPORTED_VERSION" ),
808 wxString::Format( _( "Unsupported RPC version %d." ), version ) );
809 return;
810 }
811
812 const wxString sessionId = RemoteProviderJsonString( aMessage, "session_id" );
813
814 if( sessionId.IsEmpty() )
815 {
816 sendRpcError( command, messageId, wxS( "INVALID_PARAMETERS" ),
817 _( "Missing session identifier." ) );
818 return;
819 }
820
821 if( !sessionId.IsSameAs( m_sessionId.AsString() ) )
822 {
823 sendRpcError( command, messageId, wxS( "SESSION_MISMATCH" ),
824 _( "Session identifier did not match the active provider session." ) );
825 return;
826 }
827
828 nlohmann::json params = nlohmann::json::object();
829 auto paramsIt = aMessage.find( "parameters" );
830
831 if( paramsIt != aMessage.end() && paramsIt->is_object() )
832 params = *paramsIt;
833
834 const std::string data = aMessage.value( "data", std::string() );
835
836 if( command == wxS( "NEW_SESSION" ) )
837 {
838 nlohmann::json reply = nlohmann::json::object();
839 reply["client_name"] = "KiCad";
840 reply["client_version"] = GetSemanticVersion().ToStdString();
841 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
842 sendRpcReply( command, messageId, std::move( reply ) );
843 return;
844 }
845
846 if( command == wxS( "GET_KICAD_VERSION" ) )
847 {
848 nlohmann::json reply = nlohmann::json::object();
849 reply["kicad_version"] = GetSemanticVersion().ToStdString();
850 sendRpcReply( command, messageId, std::move( reply ) );
851 return;
852 }
853
854 if( command == wxS( "LIST_SUPPORTED_VERSIONS" ) )
855 {
856 nlohmann::json reply = nlohmann::json::object();
857 reply["supported_versions"] = { REMOTE_SYMBOL_SESSION_VERSION };
858 sendRpcReply( command, messageId, std::move( reply ) );
859 return;
860 }
861
862 if( command == wxS( "CAPABILITIES" ) )
863 {
864 nlohmann::json reply = nlohmann::json::object();
865 reply["commands"] = { "NEW_SESSION", "GET_KICAD_VERSION", "LIST_SUPPORTED_VERSIONS",
866 "CAPABILITIES", "GET_SOURCE_INFO", "REMOTE_LOGIN", "DL_SYMBOL",
867 "DL_COMPONENT", "DL_FOOTPRINT", "DL_SPICE", "DL_3DMODEL",
868 "PLACE_COMPONENT" };
869 reply["compression"] = { "NONE", "ZSTD" };
870 reply["max_message_size"] = 0;
871 sendRpcReply( command, messageId, std::move( reply ) );
872 return;
873 }
874
875 if( command == wxS( "GET_SOURCE_INFO" ) )
876 {
877 nlohmann::json reply = nlohmann::json::object();
878 reply["provider_id"] = m_selectedProviderIndex == wxNOT_FOUND
879 ? std::string()
880 : m_providerEntries[m_selectedProviderIndex].provider_id.ToStdString();
881 reply["provider_name"] = m_hasSelectedProviderMetadata
882 ? m_selectedProviderMetadata.provider_name.ToStdString()
883 : std::string();
884 reply["panel_url"] = m_hasSelectedProviderMetadata
885 ? m_selectedProviderMetadata.panel_url.ToStdString()
886 : std::string();
887
888 bool authenticated = false;
889
890 if( m_selectedProviderIndex != wxNOT_FOUND )
891 authenticated = !loadAccessToken( m_providerEntries[m_selectedProviderIndex] ).IsEmpty();
892
893 reply["authenticated"] = authenticated;
894 reply["auth_type"] = m_hasSelectedProviderMetadata
896 ? "oauth2"
897 : "none" )
898 : "none";
899 reply["supports_direct_downloads"] =
901 reply["supports_inline_payloads"] =
903 sendRpcReply( command, messageId, std::move( reply ) );
904 return;
905 }
906
907 if( command == wxS( "REMOTE_LOGIN" ) )
908 {
910 {
911 sendRpcError( command, messageId, wxS( "NO_PROVIDER" ),
912 _( "No remote provider is currently selected." ) );
913 return;
914 }
915
917 const bool signOut = params.value( "sign_out", false );
918 const bool interactive = params.value( "interactive", true );
919
920 if( signOut )
921 {
922 wxString error;
923
924 if( !signOutProvider( provider, error ) )
925 {
926 sendRpcError( command, messageId, wxS( "SIGN_OUT_FAILED" ), error );
927 return;
928 }
929
931
932 nlohmann::json reply = nlohmann::json::object();
933 reply["authenticated"] = false;
934 reply["signed_out"] = true;
935 sendRpcReply( command, messageId, std::move( reply ) );
936 return;
937 }
938
939 const wxString accessToken = loadAccessToken( provider );
940
941 if( !accessToken.IsEmpty() )
942 {
943 nlohmann::json reply = nlohmann::json::object();
944 reply["authenticated"] = true;
945 reply["provider_id"] = provider.provider_id.ToStdString();
946 sendRpcReply( command, messageId, std::move( reply ) );
947 return;
948 }
949
951 {
952 nlohmann::json reply = nlohmann::json::object();
953 reply["authenticated"] = false;
954 reply["auth_type"] = "none";
955 sendRpcReply( command, messageId, std::move( reply ) );
956 return;
957 }
958
959 if( !interactive )
960 {
961 nlohmann::json reply = nlohmann::json::object();
962 reply["authenticated"] = false;
963 reply["started"] = false;
964 sendRpcReply( command, messageId, std::move( reply ) );
965 return;
966 }
967
968 wxString error;
969
970 if( !startInteractiveLogin( provider, m_selectedProviderMetadata, error ) )
971 {
972 sendRpcError( command, messageId, wxS( "LOGIN_FAILED" ), error );
973 return;
974 }
975
976 nlohmann::json reply = nlohmann::json::object();
977 reply["authenticated"] = false;
978 reply["started"] = true;
979 sendRpcReply( command, messageId, std::move( reply ) );
980 return;
981 }
982
983 if( command == wxS( "PLACE_COMPONENT" ) || command == wxS( "DL_COMPONENT" ) || command == wxS( "DL_SYMBOL" )
984 || command == wxS( "DL_FOOTPRINT" ) || command == wxS( "DL_3DMODEL" ) || command == wxS( "DL_SPICE" ) )
985 {
986 const bool placeSymbol = command == wxS( "PLACE_COMPONENT" )
987 || RemoteProviderJsonString( params, "mode" ).IsSameAs( wxS( "PLACE" ), false );
988 const bool isComponent = command == wxS( "DL_COMPONENT" ) || command == wxS( "PLACE_COMPONENT" );
989 wxString error;
990 bool ok = false;
991
992 if( isComponent && params.contains( "assets" ) && params["assets"].is_array() )
993 {
994 ok = receiveComponentManifest( params, placeSymbol, error );
995 }
996 else
997 {
998 const wxString compression = RemoteProviderJsonString( params, "compression" );
999 std::vector<uint8_t> decoded;
1000
1001 if( !decodeBase64Payload( data, decoded, error ) )
1002 {
1003 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
1004 return;
1005 }
1006
1007 std::vector<uint8_t> payload;
1008 const std::string compressionStr =
1009 compression.IsEmpty() ? std::string() : std::string( compression.ToUTF8().data() );
1010
1011 if( !decompressIfNeeded( compressionStr, decoded, payload, error ) )
1012 {
1013 sendRpcError( command, messageId, wxS( "INVALID_PAYLOAD" ), error );
1014 return;
1015 }
1016
1017 if( isComponent )
1018 ok = receiveComponent( params, payload, placeSymbol, error );
1019 else if( command == wxS( "DL_SYMBOL" ) )
1020 ok = receiveSymbol( params, payload, error );
1021 else if( command == wxS( "DL_FOOTPRINT" ) )
1022 ok = receiveFootprint( params, payload, error );
1023 else if( command == wxS( "DL_3DMODEL" ) )
1024 ok = receive3DModel( params, payload, error );
1025 else if( command == wxS( "DL_SPICE" ) )
1026 ok = receiveSPICEModel( params, payload, error );
1027 }
1028
1029 if( ok )
1030 sendRpcReply( command, messageId );
1031 else
1032 sendRpcError( command, messageId, wxS( "IMPORT_FAILED" ),
1033 error.IsEmpty() ? _( "Unable to process provider payload." ) : error );
1034
1035 return;
1036 }
1037
1038 sendRpcError( command, messageId, wxS( "UNKNOWN_COMMAND" ),
1039 wxString::Format( _( "Command '%s' is not supported." ), command ) );
1040}
1041
1042
1043bool PANEL_REMOTE_SYMBOL::receiveSymbol( const nlohmann::json& aParams,
1044 const std::vector<uint8_t>& aPayload,
1045 wxString& aError )
1046{
1047 const wxString mode = RemoteProviderJsonString( aParams, "mode" );
1048 const bool placeAfterDownload = mode.IsSameAs( wxS( "PLACE" ), false );
1049
1050 if( !mode.IsEmpty() && !mode.IsSameAs( wxS( "SAVE" ), false ) && !placeAfterDownload )
1051 {
1052 aError = wxString::Format( _( "Unsupported transfer mode '%s'." ), mode );
1053 return false;
1054 }
1055
1056 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_SYMBOL_V1" ), false ) )
1057 {
1058 aError = _( "Unsupported symbol payload type." );
1059 return false;
1060 }
1061
1062 if( !m_frame )
1063 {
1064 aError = _( "No schematic editor is available to store symbols." );
1065 return false;
1066 }
1067
1068 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1069
1070 if( !settings )
1071 {
1072 aError = _( "Unable to load schematic settings." );
1073 return false;
1074 }
1075
1076 wxFileName baseDir;
1077
1078 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1079 return false;
1080
1081 wxFileName symDir = baseDir;
1082 symDir.AppendDir( wxS( "symbols" ) );
1083 symDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1084
1085 wxString libItemName = RemoteProviderJsonString( aParams, "name" );
1086
1087 if( libItemName.IsEmpty() )
1088 libItemName = wxS( "symbol" );
1089
1090 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1091
1092 if( libraryName.IsEmpty() )
1093 libraryName = wxS( "symbols" );
1094
1095 const wxString nickname = RemoteLibraryPrefix() + wxS( "_" )
1096 + SanitizeRemoteFileComponent( libraryName, wxS( "symbols" ), true );
1097
1098 wxFileName outFile( symDir );
1099 outFile.SetFullName( nickname + wxS( ".kicad_sym" ) );
1100
1102 settings->m_RemoteSymbol.add_to_global_table, true, aError ) )
1103 return false;
1104
1106
1107 if( !adapter )
1108 {
1109 aError = _( "Unable to access the symbol library manager." );
1110 return false;
1111 }
1112
1113 std::unique_ptr<LIB_SYMBOL> downloadedSymbol = loadSymbolFromPayload( aPayload, libItemName, aError );
1114
1115 if( !downloadedSymbol )
1116 return false;
1117
1118 downloadedSymbol->SetName( libItemName );
1119 LIB_ID savedId;
1120 savedId.SetLibNickname( nickname );
1121 savedId.SetLibItemName( libItemName );
1122 downloadedSymbol->SetLibId( savedId );
1123
1124 if( adapter->SaveSymbol( nickname, downloadedSymbol.get(), true ) != SYMBOL_LIBRARY_ADAPTER::SAVE_OK )
1125 {
1126 aError = _( "Unable to save the downloaded symbol." );
1127 return false;
1128 }
1129
1134
1135 if( placeAfterDownload )
1136 return PlaceRemoteDownloadedSymbol( m_frame, nickname, libItemName, aError );
1137
1138 return true;
1139}
1140
1141
1142bool PANEL_REMOTE_SYMBOL::receiveComponent( const nlohmann::json& aParams,
1143 const std::vector<uint8_t>& aPayload,
1144 bool aPlaceSymbol, wxString& aError )
1145{
1146 nlohmann::json components;
1147
1148 try
1149 {
1150 components = nlohmann::json::parse( aPayload.begin(), aPayload.end() );
1151 }
1152 catch( const std::exception& e )
1153 {
1154 aError = wxString::Format( _( "Failed to parse component list: %s" ), e.what() );
1155 return false;
1156 }
1157
1158 if( !components.is_array() || components.empty() )
1159 {
1160 aError = _( "Component list must be a non-empty array." );
1161 return false;
1162 }
1163
1164 const wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1165
1166 for( const nlohmann::json& entry : components )
1167 {
1168 if( !entry.is_object() )
1169 {
1170 aError = _( "Component entries must be objects." );
1171 return false;
1172 }
1173
1174 std::string entryType = entry.value( "type", "" );
1175
1176 if( entryType.empty() )
1177 {
1178 aError = _( "Component entry was missing a type." );
1179 return false;
1180 }
1181
1182 std::transform( entryType.begin(), entryType.end(), entryType.begin(),
1183 []( unsigned char c ) { return static_cast<char>( std::tolower( c ) ); } );
1184
1185 std::vector<uint8_t> content;
1186
1187 if( !decodeBase64Payload( entry.value( "content", std::string() ), content, aError ) )
1188 return false;
1189
1190 const std::string compression = entry.value( "compression", std::string() );
1191
1192 if( !compression.empty() && compression != "NONE" )
1193 {
1194 std::vector<uint8_t> decoded = content;
1195
1196 if( !decompressIfNeeded( compression, decoded, content, aError ) )
1197 return false;
1198 }
1199
1200 wxString entryName = wxString::FromUTF8( entry.value( "name", "" ) );
1201 nlohmann::json entryParams = nlohmann::json::object();
1202
1203 if( !libraryName.IsEmpty() )
1204 entryParams["library"] = libraryName.ToStdString();
1205
1206 if( !entryName.IsEmpty() )
1207 entryParams["name"] = entryName.ToStdString();
1208
1209 bool ok = false;
1210
1211 if( entryType == "symbol" )
1212 {
1213 entryParams["content_type"] = "KICAD_SYMBOL_V1";
1214 entryParams["mode"] = aPlaceSymbol ? "PLACE" : "SAVE";
1215 ok = receiveSymbol( entryParams, content, aError );
1216 }
1217 else if( entryType == "footprint" )
1218 {
1219 entryParams["content_type"] = "KICAD_FOOTPRINT_V1";
1220 entryParams["mode"] = "SAVE";
1221 ok = receiveFootprint( entryParams, content, aError );
1222 }
1223 else if( entryType == "3dmodel" )
1224 {
1225 entryParams["content_type"] = "KICAD_3D_MODEL_STEP";
1226 entryParams["mode"] = "SAVE";
1227 ok = receive3DModel( entryParams, content, aError );
1228 }
1229 else if( entryType == "spice" )
1230 {
1231 entryParams["content_type"] = "KICAD_SPICE_MODEL_V1";
1232 entryParams["mode"] = "SAVE";
1233 ok = receiveSPICEModel( entryParams, content, aError );
1234 }
1235 else
1236 {
1237 aError = wxString::Format( _( "Unsupported component type '%s'." ),
1238 wxString::FromUTF8( entryType.c_str() ) );
1239 return false;
1240 }
1241
1242 if( !ok )
1243 return false;
1244 }
1245
1246 return true;
1247}
1248
1249
1250bool PANEL_REMOTE_SYMBOL::receiveFootprint( const nlohmann::json& aParams,
1251 const std::vector<uint8_t>& aPayload,
1252 wxString& aError )
1253{
1254 if( !RemoteProviderJsonString( aParams, "content_type" ).IsSameAs( wxS( "KICAD_FOOTPRINT_V1" ), false ) )
1255 {
1256 aError = _( "Unsupported footprint payload type." );
1257 return false;
1258 }
1259
1260 wxFileName baseDir;
1261
1262 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1263 return false;
1264
1265 wxFileName fpRoot = baseDir;
1266 fpRoot.AppendDir( wxS( "footprints" ) );
1267 fpRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1268
1269 wxString footprintName = SanitizeRemoteFileComponent( RemoteProviderJsonString( aParams, "name" ), wxS( "footprint" ) );
1270 wxString libraryName = RemoteProviderJsonString( aParams, "library" );
1271
1272 if( libraryName.IsEmpty() )
1273 libraryName = wxS( "footprints" );
1274
1275 const wxString libNickname = RemoteLibraryPrefix() + wxS( "_" )
1276 + SanitizeRemoteFileComponent( libraryName, wxS( "footprints" ), true );
1277
1278 wxFileName libDir = fpRoot;
1279 libDir.AppendDir( libNickname + wxS( ".pretty" ) );
1280 libDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1281
1282 if( !footprintName.Lower().EndsWith( wxS( ".kicad_mod" ) ) )
1283 footprintName += wxS( ".kicad_mod" );
1284
1285 wxFileName outFile( libDir );
1286 outFile.SetFullName( footprintName );
1287
1288 if( !WriteRemoteBinaryFile( outFile, aPayload, aError ) )
1289 return false;
1290
1291 EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
1292
1293 if( !settings )
1294 {
1295 aError = _( "Unable to load schematic settings." );
1296 return false;
1297 }
1298
1300 settings->m_RemoteSymbol.add_to_global_table, true, aError ) )
1301 return false;
1302
1307 return true;
1308}
1309
1310
1311bool PANEL_REMOTE_SYMBOL::receive3DModel( const nlohmann::json& aParams,
1312 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,
1338 const std::vector<uint8_t>& aPayload,
1339 wxString& aError )
1340{
1341 wxFileName baseDir;
1342
1343 if( !EnsureRemoteDestinationRoot( baseDir, aError ) )
1344 return false;
1345
1346 wxFileName spiceDir = baseDir;
1347 spiceDir.AppendDir( RemoteLibraryPrefix() + wxS( "_spice" ) );
1348 spiceDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1349
1350 wxString fileName = RemoteProviderJsonString( aParams, "name" );
1351
1352 if( fileName.IsEmpty() )
1353 fileName = RemoteLibraryPrefix() + wxS( "_model.cir" );
1354
1355 fileName = SanitizeRemoteFileComponent( fileName, RemoteLibraryPrefix() + wxS( "_model.cir" ) );
1356
1357 if( !fileName.Lower().EndsWith( wxS( ".cir" ) ) )
1358 fileName += wxS( ".cir" );
1359
1360 wxFileName outFile( spiceDir );
1361 outFile.SetFullName( fileName );
1362 return WriteRemoteBinaryFile( outFile, aPayload, aError );
1363}
1364
1365
1366bool PANEL_REMOTE_SYMBOL::receiveComponentManifest( const nlohmann::json& aParams,
1367 bool aPlaceSymbol, wxString& aError )
1368{
1370 {
1371 aError = _( "Remote provider metadata is not loaded." );
1372 return false;
1373 }
1374
1376 manifest.part_id = RemoteProviderJsonString( aParams, "part_id" );
1377 manifest.display_name = RemoteProviderJsonString( aParams, "display_name" );
1378 manifest.summary = RemoteProviderJsonString( aParams, "summary" );
1379 manifest.license = RemoteProviderJsonString( aParams, "license" );
1380
1381 for( const nlohmann::json& assetJson : aParams.at( "assets" ) )
1382 {
1384 asset.asset_type = RemoteProviderJsonString( assetJson, "asset_type" );
1385 asset.name = RemoteProviderJsonString( assetJson, "name" );
1386 asset.target_library = RemoteProviderJsonString( assetJson, "target_library" );
1387 asset.target_name = RemoteProviderJsonString( assetJson, "target_name" );
1388 asset.content_type = RemoteProviderJsonString( assetJson, "content_type" );
1389 asset.download_url = RemoteProviderJsonString( assetJson, "download_url" );
1390 asset.sha256 = RemoteProviderJsonString( assetJson, "sha256" );
1391 asset.required = assetJson.value( "required", false );
1392
1393 auto sizeIt = assetJson.find( "size_bytes" );
1394
1395 if( sizeIt != assetJson.end() && sizeIt->is_number_integer() )
1396 asset.size_bytes = sizeIt->get<long long>();
1397
1398 if( asset.asset_type.IsEmpty() || asset.content_type.IsEmpty() || asset.download_url.IsEmpty()
1399 || asset.size_bytes <= 0 )
1400 {
1401 aError = _( "Manifest assets require asset_type, content_type, size_bytes, and download_url." );
1402 return false;
1403 }
1404
1405 manifest.assets.push_back( asset );
1406 }
1407
1409 context.symbol_name = RemoteProviderJsonString( aParams, "symbol_name" );
1410 context.library_name = RemoteProviderJsonString( aParams, "library_name" );
1411
1412 if( context.symbol_name.IsEmpty() )
1413 {
1414 for( const REMOTE_PROVIDER_PART_ASSET& asset : manifest.assets )
1415 {
1416 if( asset.asset_type == wxS( "symbol" ) )
1417 {
1418 context.symbol_name = asset.target_name;
1419 context.library_name = asset.target_library;
1420 break;
1421 }
1422 }
1423 }
1424
1426 return job.Import( m_selectedProviderMetadata, context, manifest, aPlaceSymbol, aError );
1427}
1428
1429
1430void PANEL_REMOTE_SYMBOL::onOAuthLoopback( wxCommandEvent& aEvent )
1431{
1432 m_oauthLoopbackServer.reset();
1433
1434 if( !aEvent.GetInt() || m_pendingProviderId.IsEmpty() || !m_hasSelectedProviderMetadata )
1435 {
1436 m_pendingProviderId.clear();
1437 showMessage( _( "Remote provider sign-in was cancelled or failed." ) );
1438 return;
1439 }
1440
1441 OAUTH_TOKEN_SET tokens;
1443
1444 if( !m_providerClient.ExchangeAuthorizationCode( m_pendingOAuthMetadata, m_pendingOAuthSession,
1445 aEvent.GetString(), tokens, error ) )
1446 {
1447 m_pendingProviderId.clear();
1448 showMessage( error.message.IsEmpty() ? _( "Remote provider sign-in failed." ) : error.message );
1449 return;
1450 }
1451
1452 if( tokens.refresh_token.IsEmpty() )
1453 {
1454 if( std::optional<OAUTH_TOKEN_SET> existing = m_tokenStore.LoadTokens( m_pendingProviderId, wxS( "default" ) );
1455 existing )
1456 {
1457 tokens.refresh_token = existing->refresh_token;
1458 }
1459 }
1460
1461 if( !m_tokenStore.StoreTokens( m_pendingProviderId, wxS( "default" ), tokens ) )
1462 {
1463 m_pendingProviderId.clear();
1464 showMessage( _( "Failed to store remote provider tokens securely." ) );
1465 return;
1466 }
1467
1468 if( EESCHEMA_SETTINGS* settings = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" ) )
1469 {
1470 if( REMOTE_PROVIDER_ENTRY* provider = settings->m_RemoteSymbol.FindProviderById( m_pendingProviderId ) )
1471 {
1472 provider->last_account_label = wxS( "default" );
1473 provider->last_auth_status = wxS( "signed_in" );
1474 }
1475 }
1476
1477 m_pendingProviderId.clear();
1478
1479 if( m_selectedProviderIndex != wxNOT_FOUND )
1481}
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:49
void ReloadLibraryEntry(LIBRARY_TABLE_TYPE aType, const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH)
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:133
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...
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