You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

373 lines
11 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2024 Jon Evans <jon@craftyjon.com>
  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 along
  18. * with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. #include <magic_enum.hpp>
  21. #include <nlohmann/json.hpp>
  22. #include <wx/log.h>
  23. #include <wx/regex.h>
  24. #include <wx/stdstream.h>
  25. #include <wx/wfstream.h>
  26. #include <api/api_plugin.h>
  27. #include <api/api_plugin_manager.h>
  28. #include <api/api_utils.h>
  29. #include <json_conversions.h>
  30. #include <json_schema_validator.h>
  31. class LOGGING_ERROR_HANDLER : public nlohmann::json_schema::error_handler
  32. {
  33. public:
  34. LOGGING_ERROR_HANDLER() : m_hasError( false ) {}
  35. bool HasError() const { return m_hasError; }
  36. void error( const nlohmann::json::json_pointer& ptr, const nlohmann::json& instance,
  37. const std::string& message ) override
  38. {
  39. m_hasError = true;
  40. wxLogTrace( traceApi,
  41. wxString::Format( wxS( "JSON error: at %s, value:\n%s\n%s" ),
  42. ptr.to_string(), instance.dump(), message ) );
  43. }
  44. private:
  45. bool m_hasError;
  46. };
  47. bool PLUGIN_RUNTIME::FromJson( const nlohmann::json& aJson )
  48. {
  49. // TODO move to tl::expected and give user feedback about parse errors
  50. try
  51. {
  52. type = magic_enum::enum_cast<PLUGIN_RUNTIME_TYPE>( aJson.at( "type" ).get<std::string>(),
  53. magic_enum::case_insensitive )
  54. .value_or( PLUGIN_RUNTIME_TYPE::INVALID );
  55. }
  56. catch( ... )
  57. {
  58. return false;
  59. }
  60. return type != PLUGIN_RUNTIME_TYPE::INVALID;
  61. }
  62. struct API_PLUGIN_CONFIG
  63. {
  64. API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
  65. const JSON_SCHEMA_VALIDATOR& aValidator );
  66. bool valid;
  67. wxString identifier;
  68. wxString name;
  69. wxString description;
  70. PLUGIN_RUNTIME runtime;
  71. std::vector<PLUGIN_ACTION> actions;
  72. API_PLUGIN& parent;
  73. };
  74. API_PLUGIN_CONFIG::API_PLUGIN_CONFIG( API_PLUGIN& aParent, const wxFileName& aConfigFile,
  75. const JSON_SCHEMA_VALIDATOR& aValidator ) :
  76. parent( aParent )
  77. {
  78. valid = false;
  79. if( !aConfigFile.IsFileReadable() )
  80. return;
  81. wxLogTrace( traceApi, "Plugin: parsing config file" );
  82. wxFFileInputStream fp( aConfigFile.GetFullPath(), wxT( "rt" ) );
  83. wxStdInputStream fstream( fp );
  84. nlohmann::json js;
  85. try
  86. {
  87. js = nlohmann::json::parse( fstream, nullptr,
  88. /* allow_exceptions = */ true,
  89. /* ignore_comments = */ true );
  90. }
  91. catch( ... )
  92. {
  93. wxLogTrace( traceApi, "Plugin: exception during parse" );
  94. return;
  95. }
  96. LOGGING_ERROR_HANDLER handler;
  97. aValidator.Validate( js, handler, nlohmann::json_uri( "#/definitions/Plugin" ) );
  98. if( !handler.HasError() )
  99. wxLogTrace( traceApi, "Plugin: schema validation successful" );
  100. // All of these are required; any exceptions here leave us with valid == false
  101. try
  102. {
  103. identifier = js.at( "identifier" ).get<wxString>();
  104. name = js.at( "name" ).get<wxString>();
  105. description = js.at( "description" ).get<wxString>();
  106. if( !runtime.FromJson( js.at( "runtime" ) ) )
  107. {
  108. wxLogTrace( traceApi, "Plugin: error parsing runtime section" );
  109. return;
  110. }
  111. }
  112. catch( ... )
  113. {
  114. wxLogTrace( traceApi, "Plugin: exception while parsing required keys" );
  115. return;
  116. }
  117. if( !API_PLUGIN::IsValidIdentifier( identifier ) )
  118. {
  119. wxLogTrace( traceApi, wxString::Format( "Plugin: identifier %s does not meet requirements",
  120. identifier ) );
  121. return;
  122. }
  123. wxLogTrace( traceApi, wxString::Format( "Plugin: %s (%s)", identifier, name ) );
  124. try
  125. {
  126. const nlohmann::json& actionsJs = js.at( "actions" );
  127. if( actionsJs.is_array() )
  128. {
  129. for( const nlohmann::json& actionJs : actionsJs )
  130. {
  131. if( std::optional<PLUGIN_ACTION> a = parent.createActionFromJson( actionJs ) )
  132. {
  133. a->identifier = wxString::Format( "%s.%s", identifier, a->identifier );
  134. wxLogTrace( traceApi, wxString::Format( "Plugin: loaded action %s",
  135. a->identifier ) );
  136. actions.emplace_back( *a );
  137. }
  138. }
  139. }
  140. }
  141. catch( ... )
  142. {
  143. wxLogTrace( traceApi, "Plugin: exception while parsing actions" );
  144. }
  145. valid = true;
  146. }
  147. API_PLUGIN::API_PLUGIN( const wxFileName& aConfigFile, const JSON_SCHEMA_VALIDATOR& aValidator ) :
  148. m_configFile( aConfigFile ),
  149. m_config( std::make_unique<API_PLUGIN_CONFIG>( *this, aConfigFile, aValidator ) )
  150. {
  151. }
  152. API_PLUGIN::~API_PLUGIN()
  153. {
  154. }
  155. bool API_PLUGIN::IsOk() const
  156. {
  157. return m_config->valid;
  158. }
  159. bool API_PLUGIN::IsValidIdentifier( const wxString& aIdentifier )
  160. {
  161. // At minimum, we need a reverse-DNS style identifier with two dots and a 2+ character TLD
  162. wxRegEx identifierRegex( wxS( "[\\w\\d]{2,}\\.[\\w\\d]+\\.[\\w\\d]+" ) );
  163. return identifierRegex.Matches( aIdentifier );
  164. }
  165. const wxString& API_PLUGIN::Identifier() const
  166. {
  167. return m_config->identifier;
  168. }
  169. const wxString& API_PLUGIN::Name() const
  170. {
  171. return m_config->name;
  172. }
  173. const wxString& API_PLUGIN::Description() const
  174. {
  175. return m_config->description;
  176. }
  177. const PLUGIN_RUNTIME& API_PLUGIN::Runtime() const
  178. {
  179. return m_config->runtime;
  180. }
  181. const std::vector<PLUGIN_ACTION>& API_PLUGIN::Actions() const
  182. {
  183. return m_config->actions;
  184. }
  185. wxString API_PLUGIN::BasePath() const
  186. {
  187. return m_configFile.GetPath();
  188. }
  189. wxString API_PLUGIN::ActionSettingsKey( const PLUGIN_ACTION& aAction ) const
  190. {
  191. return Identifier() + "." + aAction.identifier;
  192. }
  193. std::optional<PLUGIN_ACTION> API_PLUGIN::createActionFromJson( const nlohmann::json& aJson )
  194. {
  195. // TODO move to tl::expected and give user feedback about parse errors
  196. PLUGIN_ACTION action( *this );
  197. try
  198. {
  199. action.identifier = aJson.at( "identifier" ).get<wxString>();
  200. wxLogTrace( traceApi, wxString::Format( "Plugin: load action %s", action.identifier ) );
  201. action.name = aJson.at( "name" ).get<wxString>();
  202. action.description = aJson.at( "description" ).get<wxString>();
  203. action.entrypoint = aJson.at( "entrypoint" ).get<wxString>();
  204. action.show_button = aJson.contains( "show-button" ) && aJson.at( "show-button" ).get<bool>();
  205. }
  206. catch( ... )
  207. {
  208. wxLogTrace( traceApi, "Plugin: exception while parsing action required keys" );
  209. return std::nullopt;
  210. }
  211. wxFileName f( action.entrypoint );
  212. if( !f.IsRelative() )
  213. {
  214. wxLogTrace( traceApi, wxString::Format( "Plugin: action contains abs path %s; skipping",
  215. action.entrypoint ) );
  216. return std::nullopt;
  217. }
  218. f.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
  219. if( !f.IsFileReadable() )
  220. {
  221. wxLogTrace( traceApi, wxString::Format( "WARNING: action entrypoint %s is not readable",
  222. f.GetFullPath() ) );
  223. }
  224. if( aJson.contains( "args" ) && aJson.at( "args" ).is_array() )
  225. {
  226. for( const nlohmann::json& argJs : aJson.at( "args" ) )
  227. {
  228. try
  229. {
  230. action.args.emplace_back( argJs.get<wxString>() );
  231. }
  232. catch( ... )
  233. {
  234. wxLogTrace( traceApi, "Plugin: exception while parsing action args" );
  235. continue;
  236. }
  237. }
  238. }
  239. if( aJson.contains( "scopes" ) && aJson.at( "scopes" ).is_array() )
  240. {
  241. for( const nlohmann::json& scopeJs : aJson.at( "scopes" ) )
  242. {
  243. try
  244. {
  245. action.scopes.insert( magic_enum::enum_cast<PLUGIN_ACTION_SCOPE>(
  246. scopeJs.get<std::string>(), magic_enum::case_insensitive )
  247. .value_or( PLUGIN_ACTION_SCOPE::INVALID ) );
  248. }
  249. catch( ... )
  250. {
  251. wxLogTrace( traceApi, "Plugin: exception while parsing action scopes" );
  252. continue;
  253. }
  254. }
  255. }
  256. auto handleBitmap =
  257. [&]( const std::string& aKey, wxBitmapBundle& aDest )
  258. {
  259. if( aJson.contains( aKey ) && aJson.at( aKey ).is_array() )
  260. {
  261. wxVector<wxBitmap> bitmaps;
  262. for( const nlohmann::json& iconJs : aJson.at( aKey ) )
  263. {
  264. wxFileName iconFile;
  265. try
  266. {
  267. iconFile = iconJs.get<wxString>();
  268. }
  269. catch( ... )
  270. {
  271. continue;
  272. }
  273. iconFile.Normalize( wxPATH_NORM_ABSOLUTE, m_configFile.GetPath() );
  274. wxLogTrace( traceApi,
  275. wxString::Format( "Plugin: action %s: loading icon %s",
  276. action.identifier, iconFile.GetFullPath() ) );
  277. if( !iconFile.IsFileReadable() )
  278. {
  279. wxLogTrace( traceApi, "Plugin: icon file could not be read" );
  280. continue;
  281. }
  282. wxBitmap bmp;
  283. // TODO: If necessary; support types other than PNG
  284. bmp.LoadFile( iconFile.GetFullPath(), wxBITMAP_TYPE_PNG );
  285. if( bmp.IsOk() )
  286. bitmaps.push_back( bmp );
  287. else
  288. wxLogTrace( traceApi, "Plugin: icon file not a valid bitmap" );
  289. }
  290. aDest = wxBitmapBundle::FromBitmaps( bitmaps );
  291. }
  292. };
  293. handleBitmap( "icons-light", action.icon_light );
  294. handleBitmap( "icons-dark", action.icon_dark );
  295. return action;
  296. }