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