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, ALLOW_EXCEPTIONS, IGNORE_COMMENTS );
119 }
120 catch( const std::exception& e )
121 {
122 wxLogTrace( traceApi, "Plugin: exception during parse" );
123 error_message = wxString::Format( _( "plugin configuration file error: %s" ),
124 wxString::FromUTF8( e.what() ) );
125 return;
126 }
127
128 LOGGING_ERROR_HANDLER handler;
129 aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
130
131 if( handler.HasError() )
132 {
133 error_message = handler.ErrorMessage();
134 return;
135 }
136
137 wxLogTrace( traceApi, "Plugin: schema validation successful" );
138
139 // All of these are required; any exceptions here leave us with valid == false
140 try
141 {
142 identifier = js.at( "identifier" ).get<wxString>();
143 name = js.at( "name" ).get<wxString>();
144 description = js.at( "description" ).get<wxString>();
145
146 if( !runtime.FromJson( js.at( "runtime" ) ).or_else(
147 [this]( const wxString& aError )
148 {
149 wxLogTrace( traceApi, "Plugin %s: %s", identifier, aError );
150 error_message = aError;
151 } ).has_value() )
152 {
153 return;
154 }
155 }
156 catch( const std::exception& e )
157 {
158 wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
159 error_message = wxString::Format( _( "missing or invalid required keys: %s" ),
160 wxString::FromUTF8( e.what() ) );
161 return;
162 }
163
165 {
166 wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
167 identifier ) );
168 error_message = wxString::Format( _( "identifier '%s' is invalid" ), identifier );
169 return;
170 }
171
172 wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
173
174 try
175 {
176 const nlohmann::json& actionsJs = js.at( "actions" );
177
178 if( actionsJs.is_array() )
179 {
180 for( const nlohmann::json& actionJs : actionsJs )
181 {
182 if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
183 {
184 a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
185 wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
186 a->identifier ) );
187 actions.emplace_back( *a );
188 }
189 }
190 }
191 }
192 catch( const std::exception& e )
193 {
194 wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
195 error_message = wxString::Format( _( "actions section is invalid: %s" ),
196 wxString::FromUTF8( e.what() ) );
197 return;
198 }
199
200 valid = true;
201 error_message = wxEmptyString;
202}
203
204
205API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
206 m_configFile( aConfigFile ),
207 m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
208{
209}
210
211
215
216
218{
219 return m_config->valid;
220}
221
222
223const wxString& API_PLUGIN::ErrorMessage() const
224{
225 return m_config->error_message;
226}
227
228
229bool API_PLUGIN::IsValidIdentifier( const wxString& aIdentifier )
230{
231 // Validate a reverse-DNS style identifier:
232 // - Starts with a TLD containing at least two letters
233 // - Requires at least two additional namespaces
234 // - Namespaces are alphanumeric and may contain internal hyphens
235 wxRegEx identifierRegex( R"(^[a-zA-Z]{2,}(\.([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]|[a-zA-Z0-9])){2,}$)" );
236 return identifierRegex.Matches( aIdentifier );
237}
238
239
240const wxString& API_PLUGIN::Identifier() const
241{
242 return m_config->identifier;
243}
244
245
246const wxString& API_PLUGIN::Name() const
247{
248 return m_config->name;
249}
250
251
252const wxString& API_PLUGIN::Description() const
253{
254 return m_config->description;
255}
256
257
259{
260 return m_config->runtime;
261}
262
263
264const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
265{
266 return m_config->actions;
267}
268
269
270wxString API_PLUGIN::BasePath() const
271{
272 return m_configFile.GetPath();
273}
274
275
276wxString API_PLUGIN::ActionSettingsKey( const PLUGIN_ACTION& aAction ) const
277{
278 return Identifier() + "." + aAction.identifier;
279}
280
281
282
283std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
284{
285 // TODO move to tl::expected and give user feedback about parse errors
286 PLUGIN_ACTION action( *this );
287
288 try
289 {
290 action.identifier = aJson.at( "identifier" ).get<wxString>();
291 wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
292 action.name = aJson.at( "name" ).get<wxString>();
293 action.description = aJson.at( "description" ).get<wxString>();
294 action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
295 action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
296 }
297 catch( ... )
298 {
299 wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
300 return std::nullopt;
301 }
302
303 wxFileName f( action.entrypoint );
304
305 if( !f.IsRelative() )
306 {
307 wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
308 action.entrypoint ) );
309 return std::nullopt;
310 }
311
312 f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
313
314 if( !f.IsFileReadable() )
315 {
316 wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
317 f.GetFullPath() ) );
318 }
319
320 if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
321 {
322 for( const nlohmann::json& argJs : aJson.at( "args" ) )
323 {
324 try
325 {
326 action.args.emplace_back( argJs.get<wxString>() );
327 }
328 catch( ... )
329 {
330 wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
331 continue;
332 }
333 }
334 }
335
336 if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
337 {
338 for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
339 {
340 try
341 {
342 action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
343 scopeJs.get<std::string>(), magic_enum::case_insensitive )
344 .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
345 }
346 catch( ... )
347 {
348 wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
349 continue;
350 }
351 }
352 }
353
354 auto handleBitmap =
355 [&]( const std::string& aKey, std::vector<wxImage>& aDest )
356 {
357 if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
358 {
359 for( const nlohmann::json& iconJs : aJson.at( aKey ) )
360 {
361 wxFileName iconFile;
362
363 try
364 {
365 iconFile = iconJs.get<wxString>();
366 }
367 catch( ... )
368 {
369 continue;
370 }
371
372 iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
373
374 wxLogTrace( traceApi,
375 wxString::Format( "Plugin: action %s: loading icon %s",
376 action.identifier, iconFile.GetFullPath() ) );
377
378
379 if( !iconFile.IsFileReadable() )
380 {
381 wxLogTrace( traceApi, "Plugin: icon file could not be read" );
382 continue;
383 }
384
385 wxImage img;
386 // TODO: If necessary; support types other than PNG
387 img.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
388
389 if( img.IsOk() )
390 aDest.push_back( img );
391 else
392 wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
393 }
394 }
395 };
396
397 handleBitmap( "icons-light", action.icon_light );
398 handleBitmap( "icons-dark", action.icon_dark );
399
400 return action;
401}
A plugin that is invoked by KiCad and runs as an external process; communicating with KiCad via the I...
Definition api_plugin.h:98
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:121
wxString ActionSettingsKey(const PLUGIN_ACTION &aAction) const
const wxString & Identifier() const
std::unique_ptr< API_PLUGIN_CONFIG > m_config
Definition api_plugin.h:127
wxFileName m_configFile
Definition api_plugin.h:125
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:148
const wxString & ErrorMessage() const
Definition api_plugin.h:150
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
std::string message
#define ALLOW_EXCEPTIONS
#define IGNORE_COMMENTS
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:74
wxString name
Definition api_plugin.h:80
wxString description
Definition api_plugin.h:81
std::set< PLUGIN_ACTION_SCOPE > scopes
Definition api_plugin.h:84
wxString identifier
Definition api_plugin.h:79
std::vector< wxImage > icon_light
Definition api_plugin.h:86
wxString entrypoint
Definition api_plugin.h:83
std::vector< wxImage > icon_dark
Definition api_plugin.h:87
std::vector< wxString > args
Definition api_plugin.h:85
tl::expected< bool, wxString > FromJson(const nlohmann::json &aJson)
PLUGIN_RUNTIME_TYPE type
Definition api_plugin.h:64
VECTOR2I location