KiCad PCB EDA Suite
Loading...
Searching...
No Matches
api_plugin.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 (C) 2024 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <magic_enum.hpp>
22#include <json_common.h>
23#include <wx/log.h>
24#include <wx/regex.h>
25#include <wx/stdstream.h>
26#include <wx/wfstream.h>
27
28#include <api/api_plugin.h>
30#include <api/api_utils.h>
31#include <json_conversions.h>
33
34
39
40
41void LOGGING_ERROR_HANDLER::error( const nlohmann::json::json_pointer& ptr,
42 const nlohmann::json& instance,
43 const std::string& message )
44{
45 m_hasError = true;
46 wxLogTrace( traceApi,
47 wxString::Format( wxS( "JSON error: at %s, value:\n%s\n%s" ),
48 ptr.to_string(), instance.dump(), message ) );
49
50 wxString location = wxString::FromUTF8( ptr.to_string() );
51
52 if( location.IsEmpty() )
53 location = wxS( "/" );
54
55 if( !m_errorMessage.IsEmpty() )
56 m_errorMessage << '\n';
57
58 m_errorMessage << wxString::Format( _( "invalid plugin configuration at '%s': %s" ),
59 location, wxString::FromUTF8( message ) );
60}
61
62
63tl::expected<bool, wxString> PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
64{
65 try
66 {
67 type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
68 magic_enum::case_insensitive )
70 }
71 catch( std::exception& e )
72 {
73 return tl::unexpected( wxString::Format( _( "invalid plugin runtime: %s" ), e.what() ) );
74 }
75
77}
78
79
81{
82 API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
83 const JSON_SCHEMA_VALIDATOR& aValidator );
84
85 bool valid;
86 wxString error_message;
87 wxString identifier;
88 wxString name;
89 wxString description;
91 std::vector<PLUGIN_ACTION> actions;
92
94};
95
96
97API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
98 const JSON_SCHEMA_VALIDATOR& aValidator ) :
99 parent( aParent )
100{
101 valid = false;
102
103 if( !aConfigFile.IsFileReadable() )
104 {
105 error_message = _( "could not read plugin configuration file" );
106 return;
107 }
108
109 wxLogTrace( traceApi, "Plugin: parsing config file" );
110
111 wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
112 wxStdInputStream fstream( fp );
113
114 nlohmann::json js;
115
116 try
117 {
118 js = nlohmann::json::parse( fstream, nullptr,
119 /* allow_exceptions = */ true,
120 /* ignore_comments = */ true );
121 }
122 catch( const std::exception& e )
123 {
124 wxLogTrace( traceApi, "Plugin: exception during parse" );
125 error_message = wxString::Format( _( "plugin configuration file error: %s" ),
126 wxString::FromUTF8( e.what() ) );
127 return;
128 }
129
130 LOGGING_ERROR_HANDLER handler;
131 aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
132
133 if( !handler.HasError() )
134 wxLogTrace( traceApi, "Plugin: schema validation successful" );
135 else
136 error_message = handler.ErrorMessage();
137
138 // All of these are required; any exceptions here leave us with valid == false
139 try
140 {
141 identifier = js.at( "identifier" ).get<wxString>();
142 name = js.at( "name" ).get<wxString>();
143 description = js.at( "description" ).get<wxString>();
144
145 if( !runtime.FromJson( js.at( "runtime" ) ).or_else(
146 [this]( const wxString& aError )
147 {
148 wxLogTrace( traceApi, "Plugin %s: %s", identifier, aError );
149 error_message = aError;
150 } ).has_value() )
151 {
152 return;
153 }
154 }
155 catch( const std::exception& e )
156 {
157 wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
158 error_message = wxString::Format( _( "missing or invalid required keys: %s" ),
159 wxString::FromUTF8( e.what() ) );
160 return;
161 }
162
164 {
165 wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
166 identifier ) );
167 error_message = wxString::Format( _( "identifier '%s' is invalid" ), identifier );
168 return;
169 }
170
171 wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
172
173 try
174 {
175 const nlohmann::json& actionsJs = js.at( "actions" );
176
177 if( actionsJs.is_array() )
178 {
179 for( const nlohmann::json& actionJs : actionsJs )
180 {
181 if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
182 {
183 a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
184 wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
185 a->identifier ) );
186 actions.emplace_back( *a );
187 }
188 }
189 }
190 }
191 catch( const std::exception& e )
192 {
193 wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
194 error_message = wxString::Format( _( "actions section is invalid: %s" ),
195 wxString::FromUTF8( e.what() ) );
196 return;
197 }
198
199 valid = true;
200 error_message = wxEmptyString;
201}
202
203
204API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
205 m_configFile( aConfigFile ),
206 m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
207{
208}
209
210
214
215
217{
218 return m_config->valid;
219}
220
221
222const wxString& API_PLUGIN::ErrorMessage() const
223{
224 return m_config->error_message;
225}
226
227
228bool API_PLUGIN::IsValidIdentifier( const wxString& aIdentifier )
229{
230 // Validate a reverse-DNS style identifier:
231 // - Starts with a TLD containing at least two letters
232 // - Requires at least two additional namespaces
233 // - Namespaces are alphanumeric and may contain internal hyphens
234 wxRegEx identifierRegex( R"(^[a-zA-Z]{2,}(\.([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]|[a-zA-Z0-9])){2,}$)" );
235 return identifierRegex.Matches( aIdentifier );
236}
237
238
239const wxString& API_PLUGIN::Identifier() const
240{
241 return m_config->identifier;
242}
243
244
245const wxString& API_PLUGIN::Name() const
246{
247 return m_config->name;
248}
249
250
251const wxString& API_PLUGIN::Description() const
252{
253 return m_config->description;
254}
255
256
258{
259 return m_config->runtime;
260}
261
262
263const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
264{
265 return m_config->actions;
266}
267
268
269wxString API_PLUGIN::BasePath() const
270{
271 return m_configFile.GetPath();
272}
273
274
275wxString API_PLUGIN::ActionSettingsKey( const PLUGIN_ACTION& aAction ) const
276{
277 return Identifier() + "." + aAction.identifier;
278}
279
280
281
282std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
283{
284 // TODO move to tl::expected and give user feedback about parse errors
285 PLUGIN_ACTION action( *this );
286
287 try
288 {
289 action.identifier = aJson.at( "identifier" ).get<wxString>();
290 wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
291 action.name = aJson.at( "name" ).get<wxString>();
292 action.description = aJson.at( "description" ).get<wxString>();
293 action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
294 action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
295 }
296 catch( ... )
297 {
298 wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
299 return std::nullopt;
300 }
301
302 wxFileName f( action.entrypoint );
303
304 if( !f.IsRelative() )
305 {
306 wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
307 action.entrypoint ) );
308 return std::nullopt;
309 }
310
311 f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
312
313 if( !f.IsFileReadable() )
314 {
315 wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
316 f.GetFullPath() ) );
317 }
318
319 if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
320 {
321 for( const nlohmann::json& argJs : aJson.at( "args" ) )
322 {
323 try
324 {
325 action.args.emplace_back( argJs.get<wxString>() );
326 }
327 catch( ... )
328 {
329 wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
330 continue;
331 }
332 }
333 }
334
335 if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
336 {
337 for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
338 {
339 try
340 {
341 action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
342 scopeJs.get<std::string>(), magic_enum::case_insensitive )
343 .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
344 }
345 catch( ... )
346 {
347 wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
348 continue;
349 }
350 }
351 }
352
353 auto handleBitmap =
354 [&]( const std::string& aKey, wxBitmapBundle& aDest )
355 {
356 if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
357 {
358 wxVector<wxBitmap> bitmaps;
359
360 for( const nlohmann::json& iconJs : aJson.at( aKey ) )
361 {
362 wxFileName iconFile;
363
364 try
365 {
366 iconFile = iconJs.get<wxString>();
367 }
368 catch( ... )
369 {
370 continue;
371 }
372
373 iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
374
375 wxLogTrace( traceApi,
376 wxString::Format( "Plugin: action %s: loading icon %s",
377 action.identifier, iconFile.GetFullPath() ) );
378
379
380 if( !iconFile.IsFileReadable() )
381 {
382 wxLogTrace( traceApi, "Plugin: icon file could not be read" );
383 continue;
384 }
385
386 wxBitmap bmp;
387 // TODO: If necessary; support types other than PNG
388 bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
389
390 if( bmp.IsOk() )
391 bitmaps.push_back( bmp );
392 else
393 wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
394 }
395
396 aDest = wxBitmapBundle::FromBitmaps( bitmaps );
397 }
398 };
399
400 handleBitmap( "icons-light", action.icon_light );
401 handleBitmap( "icons-dark", action.icon_dark );
402
403 return action;
404}
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
Definition api_plugin.h:96
const PLUGIN_RUNTIME & Runtime() const
const wxString & Name() const
const std::vector< PLUGIN_ACTION > & Actions() const
friend struct API_PLUGIN_CONFIG
Definition api_plugin.h:119
wxString ActionSettingsKey(const PLUGIN_ACTION &aAction) const
const wxString & Identifier() const
std::unique_ptr< API_PLUGIN_CONFIG > m_config
Definition api_plugin.h:125
wxFileName m_configFile
Definition api_plugin.h:123
bool IsOk() const
wxString BasePath() const
static bool IsValidIdentifier(const wxString &aIdentifier)
const wxString & Description() const
API_PLUGIN(const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
std::optional< PLUGIN_ACTION > createActionFromJson(const nlohmann::json &aJson)
const wxString & ErrorMessage() const
Code outside of kicommon must validate through this class rather than instantiating nlohmann::json_sc...
nlohmann::json Validate(const nlohmann::json &aJson, nlohmann::json_schema::error_handler &aErrorHandler, const nlohmann::json_uri &aInitialUri=nlohmann::json_uri("#")) const
bool HasError() const
Definition api_plugin.h:146
const wxString & ErrorMessage() const
Definition api_plugin.h:148
void error(const nlohmann::json::json_pointer &ptr, const nlohmann::json &instance, const std::string &message) override
#define _(s)
const wxChar *const traceApi
Flag to enable debug output related to the IPC API and its plugin system.
Definition api_utils.cpp:33
STL namespace.
API_PLUGIN_CONFIG(API_PLUGIN &aParent, const wxFileName &aConfigFile, const JSON_SCHEMA_VALIDATOR &aValidator)
wxString error_message
PLUGIN_RUNTIME runtime
API_PLUGIN & parent
std::vector< PLUGIN_ACTION > actions
An action performed by a plugin via the IPC API.
Definition api_plugin.h:72
wxBitmapBundle icon_light
Definition api_plugin.h:84
wxString name
Definition api_plugin.h:78
wxString description
Definition api_plugin.h:79
std::set< PLUGIN_ACTION_SCOPE > scopes
Definition api_plugin.h:82
wxString identifier
Definition api_plugin.h:77
wxString entrypoint
Definition api_plugin.h:81
wxBitmapBundle icon_dark
Definition api_plugin.h:85
std::vector< wxString > args
Definition api_plugin.h:83
tl::expected< bool, wxString > FromJson(const nlohmann::json &aJson)
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:62
VECTOR2I location