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 along
18 * with this program. If not, see <http://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
222bool HTTP_LIB_CONNECTION::SelectOne( const std::string& aPartID, HTTP_LIB_PART& aFetchedPart )
223{
224 if( !IsValidEndpoint() )
225 {
226 wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) );
227 return false;
228 }
229
230 // Check if there is already a part in our cache, if not fetch it
231 if( m_cachedParts.find( aPartID ) != m_cachedParts.end() )
232 {
233 // check if it's outdated, if so re-fetch
234 if( std::difftime( std::time( nullptr ), m_cachedParts[aPartID].lastCached ) < m_source.timeout_parts )
235 {
236 aFetchedPart = m_cachedParts[aPartID];
237 return true;
238 }
239 }
240
241 std::string res = "";
242
243 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
244 std::string url = m_source.root_url + fmt::format( "parts/{}.json", aPartID );
245 curl->SetURL( url );
246
247 try
248 {
249 curl->Perform();
250
251 res = curl->GetBuffer();
252
253 if( !checkServerResponse( curl ) )
254 return false;
255
256 nlohmann::ordered_json response = nlohmann::ordered_json::parse( res );
257 std::string key = "";
258 std::string value = "";
259
260 // get a timestamp for caching
261 aFetchedPart.lastCached = std::time( nullptr );
262
263 setPartIdNameAndMetadata( response, aFetchedPart );
264
265 aFetchedPart.symbolIdStr = response.at( "symbolIdStr" );
266
267 // initially assume no exclusion
268 std::string exclude;
269
270 if( response.contains( "exclude_from_bom" ) )
271 {
272 // if key value doesn't exists default to false
273 exclude = response.at( "exclude_from_bom" );
274 aFetchedPart.exclude_from_bom = boolFromString( exclude, false );
275 }
276
277 // initially assume no exclusion
278 if( response.contains( "exclude_from_board" ) )
279 {
280 // if key value doesn't exists default to false
281 exclude = response.at( "exclude_from_board" );
282 aFetchedPart.exclude_from_board = boolFromString( exclude, false );
283 }
284
285 // initially assume no exclusion
286 if( response.contains( "exclude_from_sim" ) )
287 {
288 // if key value doesn't exists default to false
289 exclude = response.at( "exclude_from_sim" );
290 aFetchedPart.exclude_from_sim = boolFromString( exclude, false );
291 }
292
293 // remove previously loaded fields
294 aFetchedPart.fields.clear();
295
296 // Extract available fields
297 for( const auto& field : response.at( "fields" ).items() )
298 {
299 bool visible = true;
300
301 // name of the field
302 key = field.key();
303
304 // this is a dict
305 auto& properties = field.value();
306
307 value = properties.at( "value" );
308
309 // check if user wants to display field in schematic
310 if( properties.contains( "visible" ) )
311 {
312 std::string vis = properties.at( "visible" );
313 visible = boolFromString( vis, true );
314 }
315
316 // Add field to fields list
317 aFetchedPart.fields.push_back( std::make_pair( key, std::make_tuple( value, visible ) ) );
318 }
319 }
320 catch( const std::exception& e )
321 {
322 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
323 e.what(), res );
324
325 wxLogTrace( traceHTTPLib, wxT( "SelectOne: Exception while fetching part: %s" ), m_lastError );
326
327 return false;
328 }
329
330 m_cachedParts[aFetchedPart.id] = aFetchedPart;
331
332 return true;
333}
334
335
336bool HTTP_LIB_CONNECTION::SelectAll( const HTTP_LIB_CATEGORY& aCategory, std::vector<HTTP_LIB_PART>& aParts )
337{
338 if( !IsValidEndpoint() )
339 {
340 wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) );
341 return false;
342 }
343
344 std::string res = "";
345
346 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
347
348 curl->SetURL( m_source.root_url + fmt::format( "parts/category/{}.json", aCategory.id ) );
349
350 try
351 {
352 curl->Perform();
353
354 res = curl->GetBuffer();
355
356 nlohmann::json response = nlohmann::json::parse( res );
357
358 for( nlohmann::json& item : response )
359 {
360 //PART result;
361 HTTP_LIB_PART part;
362
363 setPartIdNameAndMetadata( item, part );
364
365 // add to cache
366 m_cache[part.name] = std::make_tuple( part.id, aCategory.id );
367
368 aParts.emplace_back( std::move( part ) );
369 }
370 }
371 catch( const std::exception& e )
372 {
373 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
374 e.what(), res );
375
376 wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts: %s" ), m_lastError );
377
378 return false;
379 }
380
381 return true;
382}
383
384
385bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr<KICAD_CURL_EASY>& aCurl )
386{
387 int statusCode = aCurl->GetResponseStatusCode();
388
389 if( statusCode != 200 )
390 {
391 m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n",
392 httpErrorCodeDescription( statusCode ) );
393 return false;
394 }
395
396 return true;
397}
398
399
401{
402 auto codeDescription =
403 []( uint16_t aCode ) -> wxString
404 {
405 switch( aCode )
406 {
407 case 100: return wxS( "Continue" );
408 case 101: return wxS( "Switching Protocols" );
409 case 102: return wxS( "Processing" );
410 case 103: return wxS( "Early Hints" );
411
412 case 200: return wxS( "OK" );
413 case 201: return wxS( "Created" );
414 case 203: return wxS( "Non-Authoritative Information" );
415 case 204: return wxS( "No Content" );
416 case 205: return wxS( "Reset Content" );
417 case 206: return wxS( "Partial Content" );
418 case 207: return wxS( "Multi-Status" );
419 case 208: return wxS( "Already Reported" );
420 case 226: return wxS( "IM Used" );
421
422 case 300: return wxS( "Multiple Choices" );
423 case 301: return wxS( "Moved Permanently" );
424 case 302: return wxS( "Found" );
425 case 303: return wxS( "See Other" );
426 case 304: return wxS( "Not Modified" );
427 case 305: return wxS( "Use Proxy (Deprecated)" );
428 case 306: return wxS( "Unused" );
429 case 307: return wxS( "Temporary Redirect" );
430 case 308: return wxS( "Permanent Redirect" );
431
432 case 400: return wxS( "Bad Request" );
433 case 401: return wxS( "Unauthorized" );
434 case 402: return wxS( "Payment Required (Experimental)" );
435 case 403: return wxS( "Forbidden" );
436 case 404: return wxS( "Not Found" );
437 case 405: return wxS( "Method Not Allowed" );
438 case 406: return wxS( "Not Acceptable" );
439 case 407: return wxS( "Proxy Authentication Required" );
440 case 408: return wxS( "Request Timeout" );
441 case 409: return wxS( "Conflict" );
442 case 410: return wxS( "Gone" );
443 case 411: return wxS( "Length Required" );
444 case 412: return wxS( "Payload Too Large" );
445 case 414: return wxS( "URI Too Long" );
446 case 415: return wxS( "Unsupported Media Type" );
447 case 416: return wxS( "Range Not Satisfiable" );
448 case 417: return wxS( "Expectation Failed" );
449 case 418: return wxS( "I'm a teapot" );
450 case 421: return wxS( "Misdirected Request" );
451 case 422: return wxS( "Unprocessable Content" );
452 case 423: return wxS( "Locked" );
453 case 424: return wxS( "Failed Dependency" );
454 case 425: return wxS( "Too Early (Experimental)" );
455 case 426: return wxS( "Upgrade Required" );
456 case 428: return wxS( "Precondition Required" );
457 case 429: return wxS( "Too Many Requests" );
458 case 431: return wxS( "Request Header Fields Too Large" );
459 case 451: return wxS( "Unavailable For Legal Reasons" );
460
461 case 500: return wxS( "Internal Server Error" );
462 case 501: return wxS( "Not Implemented" );
463 case 502: return wxS( "Bad Gateway" );
464 case 503: return wxS( "Service Unavailable" );
465 case 504: return wxS( "Gateway Timeout" );
466 case 505: return wxS( "HTTP Version Not Supported" );
467 case 506: return wxS( "Variant Also Negotiates" );
468 case 507: return wxS( "Insufficient Storage" );
469 case 508: return wxS( "Loop Detected" );
470 case 510: return wxS( "Not Extended" );
471 case 511: return wxS( "Network Authentication Required" );
472 default: return wxS( "Unknown" );
473 }
474 };
475
476 return wxString::Format( wxS( "%d: %s" ), aHttpCode, codeDescription( aHttpCode ) );
477}
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:192
const char * c_str() const
Definition utf8.h:109
#define _(s)
void setPartIdNameAndMetadata(const nlohmann::json &aPart_json, HTTP_LIB_PART &aPart)
const char *const traceHTTPLib
bool boolFromString(const std::any &aVal, bool aDefaultValue)
const char *const traceHTTPLib
std::string id
id of category
std::string name
name of category
std::string description
description of category
std::string symbolIdStr
std::string keywords
std::vector< std::string > fp_filters
std::time_t lastCached
std::vector< std::pair< std::string, std::tuple< std::string, bool > > > fields
VECTOR3I res