KiCad PCB EDA Suite
Loading...
Searching...
No Matches
http_lib_connection.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) 2023 Andre F. K. Iwers <[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 <wx/log.h>
22#include <fmt/core.h>
23#include <wx/translation.h>
24#include <ctime>
25
26#include <boost/algorithm/string.hpp>
27#include <json_common.h>
28#include <wx/base64.h>
29
31#include <curl/curl.h>
32
34#include <lib_id.h>
35
36const char* const traceHTTPLib = "KICAD_HTTP_LIB";
37
38
39HTTP_LIB_CONNECTION::HTTP_LIB_CONNECTION( const HTTP_LIB_SOURCE& aSource, bool aTestConnectionNow )
40{
41 m_source = aSource;
42
43 if( aTestConnectionNow )
45}
46
47
49{
50 m_endpointValid = false;
51 std::string res = "";
52
53 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
54 curl->SetURL( m_source.root_url );
55
56 try
57 {
58 curl->Perform();
59
60 res = curl->GetBuffer();
61
62 if( !checkServerResponse( curl ) )
63 return false;
64
65 if( res.length() == 0 )
66 {
67 m_lastError += wxString::Format( _( "KiCad received an empty response!" ) + "\n" );
68 }
69 else
70 {
71 nlohmann::json response = nlohmann::json::parse( res );
72
73 // Check that the endpoints exist, if not fail.
74 if( !response.at( "categories" ).empty() && !response.at( "parts" ).empty() )
75 m_endpointValid = true;
76 }
77 }
78 catch( const std::exception& e )
79 {
80 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
81 e.what(), res );
82
83 wxLogTrace( traceHTTPLib, wxT( "validateHttpLibraryEndpoints: Exception while testing API connection: %s" ),
85
86 m_endpointValid = false;
87 }
88
89 if( m_endpointValid )
91
92 return m_endpointValid;
93}
94
95
97{
98 if( !IsValidEndpoint() )
99 {
100 wxLogTrace( traceHTTPLib, wxT( "syncCategories: without valid connection!" ) );
101 return false;
102 }
103
104 std::string res = "";
105
106 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
107 curl->SetURL( m_source.root_url + "categories.json" );
108
109 try
110 {
111 curl->Perform();
112
113 res = curl->GetBuffer();
114
115 if( !checkServerResponse( curl ) )
116 return false;
117
118 nlohmann::json response = nlohmann::json::parse( res );
119
120 // collect the categories in vector
121 for( const auto& item : response.items() )
122 {
123 HTTP_LIB_CATEGORY category;
124
125 auto& value = item.value();
126 category.id = value["id"].get<std::string>();
127 category.name = value["name"].get<std::string>();
128
129 if( value.contains( "description" ) )
130 {
131 category.description = value["description"].get<std::string>();
132 m_categoryDescriptions[category.name] = category.description;
133 }
134
135 m_categories.push_back( category );
136 }
137 }
138 catch( const std::exception& e )
139 {
140 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
141 e.what(), res );
142
143 wxLogTrace( traceHTTPLib, wxT( "syncCategories: Exception while syncing categories: %s" ), m_lastError );
144
145 m_categories.clear();
146
147 return false;
148 }
149
150 return true;
151}
152
153
154bool boolFromString( const std::any& aVal, bool aDefaultValue )
155{
156 try
157 {
158 wxString strval( std::any_cast<std::string>( aVal ).c_str(), wxConvUTF8 );
159
160 if( strval.IsEmpty() )
161 return aDefaultValue;
162
163 strval.MakeLower();
164
165 for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } )
166 {
167 if( strval.Matches( trueVal ) )
168 return true;
169 }
170
171 for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } )
172 {
173 if( strval.Matches( falseVal ) )
174 return false;
175 }
176 }
177 catch( const std::bad_any_cast& )
178 {
179 }
180
181 return aDefaultValue;
182}
183
184
185void setPartIdNameAndMetadata( const nlohmann::json& aPart_json, HTTP_LIB_PART& aPart )
186{
187 // the id used to identify the part, the name is needed to show a human-readable
188 // part description to the user inside the symbol chooser dialog
189 aPart.id = aPart_json.at( "id" );
190
191 // API might not want to return an optional name.
192 if( aPart_json.contains( "name" ) )
193 aPart.name = aPart_json.at( "name" );
194 else
195 aPart.name = aPart.id;
196
197 aPart.name = LIB_ID::FixIllegalChars( aPart.name, false ).c_str();
198
199 if( aPart_json.contains( "description" ) )
200 aPart.desc = aPart_json.at( "description" );
201
202 if( aPart_json.contains( "keywords" ) )
203 aPart.keywords = aPart_json.at( "keywords" );
204
205 if( aPart_json.contains( "footprint_filters" ) )
206 {
207 nlohmann::json filters_json = aPart_json.at( "footprint_filters" );
208
209 if( filters_json.is_array() )
210 {
211 for( const auto& val : filters_json )
212 aPart.fp_filters.push_back( val );
213 }
214 else
215 {
216 aPart.fp_filters.push_back( filters_json );
217 }
218 }
219}
220
221
222// The API is loosely specified and some servers return the exclusion flags and field
223// visibility as native JSON booleans rather than strings, so accept either form.
224static bool jsonBoolField( const nlohmann::json& aValue, bool aDefault )
225{
226 if( aValue.is_boolean() )
227 return aValue.get<bool>();
228
229 if( aValue.is_string() )
230 return boolFromString( aValue.get<std::string>(), aDefault );
231
232 return aDefault;
233}
234
235
236bool setPartExtendedData( const nlohmann::json& aPartJson, HTTP_LIB_PART& aPart )
237{
238 if( aPartJson.contains( "symbolIdStr" ) && aPartJson.at( "symbolIdStr" ).is_string() )
239 aPart.symbolIdStr = aPartJson.at( "symbolIdStr" ).get<std::string>();
240
241 if( aPartJson.contains( "exclude_from_bom" ) )
242 aPart.exclude_from_bom = jsonBoolField( aPartJson.at( "exclude_from_bom" ), false );
243
244 if( aPartJson.contains( "exclude_from_board" ) )
245 aPart.exclude_from_board = jsonBoolField( aPartJson.at( "exclude_from_board" ), false );
246
247 if( aPartJson.contains( "exclude_from_sim" ) )
248 aPart.exclude_from_sim = jsonBoolField( aPartJson.at( "exclude_from_sim" ), false );
249
250 if( !aPartJson.contains( "fields" ) || !aPartJson.at( "fields" ).is_object() )
251 return false;
252
253 aPart.fields.clear();
254
255 for( const auto& field : aPartJson.at( "fields" ).items() )
256 {
257 const nlohmann::json& properties = field.value();
258
259 if( !properties.is_object() || !properties.contains( "value" )
260 || !properties.at( "value" ).is_string() )
261 {
262 continue;
263 }
264
265 std::string value = properties.at( "value" ).get<std::string>();
266 bool visible = true;
267
268 if( properties.contains( "visible" ) )
269 visible = jsonBoolField( properties.at( "visible" ), true );
270
271 aPart.fields.emplace_back( field.key(), std::make_tuple( value, visible ) );
272 }
273
274 return true;
275}
276
277
278bool HTTP_LIB_CONNECTION::SelectOne( const std::string& aPartID, HTTP_LIB_PART& aFetchedPart )
279{
280 if( !IsValidEndpoint() )
281 {
282 wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) );
283 return false;
284 }
285
286 // Check if there is already a part in our cache, if not fetch it
287 if( m_cachedParts.find( aPartID ) != m_cachedParts.end() )
288 {
289 // check if it's outdated, if so re-fetch
290 if( std::difftime( std::time( nullptr ), m_cachedParts[aPartID].lastCached ) < m_source.timeout_parts )
291 {
292 aFetchedPart = m_cachedParts[aPartID];
293 return true;
294 }
295 }
296
297 std::string res = "";
298
299 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
300 std::string url = m_source.root_url + fmt::format( "parts/{}.json", aPartID );
301 curl->SetURL( url );
302
303 try
304 {
305 curl->Perform();
306
307 res = curl->GetBuffer();
308
309 if( !checkServerResponse( curl ) )
310 return false;
311
312 nlohmann::ordered_json response = nlohmann::ordered_json::parse( res );
313
314 // get a timestamp for caching
315 aFetchedPart.lastCached = std::time( nullptr );
316
317 setPartIdNameAndMetadata( response, aFetchedPart );
318 setPartExtendedData( response, aFetchedPart );
319
320 // parse optional pin assignments (legacy flat form; issue #2282)
321 aFetchedPart.pin_map.clear();
322
323 if( response.contains( "pin_map" ) )
324 aFetchedPart.pin_map = ParseLegacyPinAssignments( response["pin_map"] );
325
326 // parse the spec-form named pin maps + footprint associations (issue #2282)
327 aFetchedPart.named_pin_maps = ParsePinMapSet( response );
328 aFetchedPart.associated_footprints = ParseAssociatedFootprints( response );
329
330 // Reaching the per-part endpoint means we have the full record, even if the
331 // server returned no fields for this part; otherwise it would be re-fetched forever.
332 aFetchedPart.detailsLoaded = true;
333 }
334 catch( const std::exception& e )
335 {
336 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
337 e.what(), res );
338
339 wxLogTrace( traceHTTPLib, wxT( "SelectOne: Exception while fetching part: %s" ), m_lastError );
340
341 return false;
342 }
343
344 m_cachedParts[aFetchedPart.id] = aFetchedPart;
345
346 return true;
347}
348
349
350bool HTTP_LIB_CONNECTION::SelectAll( const HTTP_LIB_CATEGORY& aCategory, std::vector<HTTP_LIB_PART>& aParts )
351{
352 if( !IsValidEndpoint() )
353 {
354 wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) );
355 return false;
356 }
357
358 std::string res = "";
359
360 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
361
362 curl->SetURL( m_source.root_url + fmt::format( "parts/category/{}.json", aCategory.id ) );
363
364 try
365 {
366 curl->Perform();
367
368 res = curl->GetBuffer();
369
370 nlohmann::json response = nlohmann::json::parse( res );
371
372 for( nlohmann::json& item : response )
373 {
374 HTTP_LIB_PART part;
375
376 setPartIdNameAndMetadata( item, part );
377
378 // Some servers include the full field set in the category listing; when they do,
379 // the chooser can show field content without a per-part fetch.
380 part.detailsLoaded = setPartExtendedData( item, part );
381
382 m_cache[part.name] = std::make_tuple( part.id, aCategory.id );
383
384 if( part.detailsLoaded )
385 {
386 part.lastCached = std::time( nullptr );
387 m_cachedParts[part.id] = part;
388 }
389
390 aParts.emplace_back( std::move( part ) );
391 }
392 }
393 catch( const std::exception& e )
394 {
395 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
396 e.what(), res );
397
398 wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts: %s" ), m_lastError );
399
400 return false;
401 }
402
403 return true;
404}
405
406
407bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr<KICAD_CURL_EASY>& aCurl )
408{
409 int statusCode = aCurl->GetResponseStatusCode();
410
411 if( statusCode != 200 )
412 {
413 m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n",
414 httpErrorCodeDescription( statusCode ) );
415 return false;
416 }
417
418 return true;
419}
420
421
423{
424 auto codeDescription =
425 []( uint16_t aCode ) -> wxString
426 {
427 switch( aCode )
428 {
429 case 100: return wxS( "Continue" );
430 case 101: return wxS( "Switching Protocols" );
431 case 102: return wxS( "Processing" );
432 case 103: return wxS( "Early Hints" );
433
434 case 200: return wxS( "OK" );
435 case 201: return wxS( "Created" );
436 case 203: return wxS( "Non-Authoritative Information" );
437 case 204: return wxS( "No Content" );
438 case 205: return wxS( "Reset Content" );
439 case 206: return wxS( "Partial Content" );
440 case 207: return wxS( "Multi-Status" );
441 case 208: return wxS( "Already Reported" );
442 case 226: return wxS( "IM Used" );
443
444 case 300: return wxS( "Multiple Choices" );
445 case 301: return wxS( "Moved Permanently" );
446 case 302: return wxS( "Found" );
447 case 303: return wxS( "See Other" );
448 case 304: return wxS( "Not Modified" );
449 case 305: return wxS( "Use Proxy (Deprecated)" );
450 case 306: return wxS( "Unused" );
451 case 307: return wxS( "Temporary Redirect" );
452 case 308: return wxS( "Permanent Redirect" );
453
454 case 400: return wxS( "Bad Request" );
455 case 401: return wxS( "Unauthorized" );
456 case 402: return wxS( "Payment Required (Experimental)" );
457 case 403: return wxS( "Forbidden" );
458 case 404: return wxS( "Not Found" );
459 case 405: return wxS( "Method Not Allowed" );
460 case 406: return wxS( "Not Acceptable" );
461 case 407: return wxS( "Proxy Authentication Required" );
462 case 408: return wxS( "Request Timeout" );
463 case 409: return wxS( "Conflict" );
464 case 410: return wxS( "Gone" );
465 case 411: return wxS( "Length Required" );
466 case 412: return wxS( "Payload Too Large" );
467 case 414: return wxS( "URI Too Long" );
468 case 415: return wxS( "Unsupported Media Type" );
469 case 416: return wxS( "Range Not Satisfiable" );
470 case 417: return wxS( "Expectation Failed" );
471 case 418: return wxS( "I'm a teapot" );
472 case 421: return wxS( "Misdirected Request" );
473 case 422: return wxS( "Unprocessable Content" );
474 case 423: return wxS( "Locked" );
475 case 424: return wxS( "Failed Dependency" );
476 case 425: return wxS( "Too Early (Experimental)" );
477 case 426: return wxS( "Upgrade Required" );
478 case 428: return wxS( "Precondition Required" );
479 case 429: return wxS( "Too Many Requests" );
480 case 431: return wxS( "Request Header Fields Too Large" );
481 case 451: return wxS( "Unavailable For Legal Reasons" );
482
483 case 500: return wxS( "Internal Server Error" );
484 case 501: return wxS( "Not Implemented" );
485 case 502: return wxS( "Bad Gateway" );
486 case 503: return wxS( "Service Unavailable" );
487 case 504: return wxS( "Gateway Timeout" );
488 case 505: return wxS( "HTTP Version Not Supported" );
489 case 506: return wxS( "Variant Also Negotiates" );
490 case 507: return wxS( "Insufficient Storage" );
491 case 508: return wxS( "Loop Detected" );
492 case 510: return wxS( "Not Extended" );
493 case 511: return wxS( "Network Authentication Required" );
494 default: return wxS( "Unknown" );
495 }
496 };
497
498 return wxString::Format( wxS( "%d: %s" ), aHttpCode, codeDescription( aHttpCode ) );
499}
bool checkServerResponse(std::unique_ptr< KICAD_CURL_EASY > &aCurl)
std::map< std::string, HTTP_LIB_PART > m_cachedParts
std::unique_ptr< KICAD_CURL_EASY > createCurlEasyObject()
std::map< std::string, std::string > m_categoryDescriptions
bool SelectAll(const HTTP_LIB_CATEGORY &aCategory, std::vector< HTTP_LIB_PART > &aParts)
Retrieve all parts from a specific category from the HTTP library.
HTTP_LIB_CONNECTION(const HTTP_LIB_SOURCE &aSource, bool aTestConnectionNow)
std::map< std::string, std::tuple< std::string, std::string > > m_cache
std::vector< HTTP_LIB_CATEGORY > m_categories
bool SelectOne(const std::string &aPartID, HTTP_LIB_PART &aFetchedPart)
Retrieve a single part with full details from the HTTP library.
wxString httpErrorCodeDescription(uint16_t aHttpCode)
HTTP response status codes indicate whether a specific HTTP request has been successfully completed.
static UTF8 FixIllegalChars(const UTF8 &aLibItemName, bool aLib)
Replace illegal LIB_ID item name characters with underscores '_'.
Definition lib_id.cpp:205
const char * c_str() const
Definition utf8.h:104
#define _(s)
void setPartIdNameAndMetadata(const nlohmann::json &aPart_json, HTTP_LIB_PART &aPart)
const char *const traceHTTPLib
bool setPartExtendedData(const nlohmann::json &aPartJson, HTTP_LIB_PART &aPart)
Parse the exclusion flags and field content from a part's JSON record into aPart.
bool boolFromString(const std::any &aVal, bool aDefaultValue)
static bool jsonBoolField(const nlohmann::json &aValue, bool aDefault)
const char *const traceHTTPLib
bool setPartExtendedData(const nlohmann::json &aPartJson, HTTP_LIB_PART &aPart)
Parse the exclusion flags and field content from a part's JSON record into aPart.
std::vector< ASSOCIATED_FOOTPRINT > ParseAssociatedFootprints(const nlohmann::json &aParent)
Parse the spec-form footprint associations from aParent["associated_footprints"] (issue #2282).
Definition pin_map.cpp:254
std::unordered_map< wxString, std::vector< wxString > > ParseLegacyPinAssignments(const nlohmann::json &aArray)
Parse the legacy flat pin-assignment JSON array (MR !2540 form) into a symbol-pin to footprint-pad(s)...
Definition pin_map.cpp:187
PIN_MAP_SET ParsePinMapSet(const nlohmann::json &aParent)
Parse the spec-form named maps from aParent["pin_maps"] into a PIN_MAP_SET (issue #2282).
Definition pin_map.cpp:221
std::string id
id of category
std::string name
name of category
std::string description
description of category
std::vector< ASSOCIATED_FOOTPRINT > associated_footprints
std::string symbolIdStr
std::unordered_map< wxString, std::vector< wxString > > pin_map
Legacy flat MR !2540 pin assignment table (read for one release; issue #2282).
std::string keywords
std::vector< std::string > fp_filters
std::time_t lastCached
std::vector< std::pair< std::string, field_type > > fields
PIN_MAP_SET named_pin_maps
Spec-form named pin maps and their footprint associations (issue #2282).
VECTOR3I res