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 HTTP_LIB_CONNECTION::SelectOne( const std::string& aPartID, HTTP_LIB_PART& aFetchedPart )
155{
156 if( !IsValidEndpoint() )
157 {
158 wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) );
159 return false;
160 }
161
162 // Check if there is already a part in our cache, if not fetch it
163 if( m_cachedParts.find( aPartID ) != m_cachedParts.end() )
164 {
165 // check if it's outdated, if so re-fetch
166 if( std::difftime( std::time( nullptr ), m_cachedParts[aPartID].lastCached ) < m_source.timeout_parts )
167 {
168 aFetchedPart = m_cachedParts[aPartID];
169 return true;
170 }
171 }
172
173 std::string res = "";
174
175 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
176 std::string url = m_source.root_url + fmt::format( "parts/{}.json", aPartID );
177 curl->SetURL( url );
178
179 try
180 {
181 curl->Perform();
182
183 res = curl->GetBuffer();
184
185 if( !checkServerResponse( curl ) )
186 return false;
187
188 nlohmann::ordered_json response = nlohmann::ordered_json::parse( res );
189 std::string key = "";
190 std::string value = "";
191
192 // the id used to identify the part, the name is needed to show a human-readable
193 // part description to the user inside the symbol chooser dialog
194 aFetchedPart.id = response.at( "id" );
195
196 // get a timestamp for caching
197 aFetchedPart.lastCached = std::time( nullptr );
198
199 // API might not want to return an optional name.
200 if( response.contains( "name" ) )
201 aFetchedPart.name = response.at( "name" );
202 else
203 aFetchedPart.name = aFetchedPart.id;
204
205 UTF8 sanitizedFetchedName = LIB_ID::FixIllegalChars( aFetchedPart.name, false );
206 aFetchedPart.name = sanitizedFetchedName.c_str();
207
208 aFetchedPart.symbolIdStr = response.at( "symbolIdStr" );
209
210 // initially assume no exclusion
211 std::string exclude;
212
213 if( response.contains( "exclude_from_bom" ) )
214 {
215 // if key value doesn't exists default to false
216 exclude = response.at( "exclude_from_bom" );
217 aFetchedPart.exclude_from_bom = boolFromString( exclude, false );
218 }
219
220 // initially assume no exclusion
221 if( response.contains( "exclude_from_board" ) )
222 {
223 // if key value doesn't exists default to false
224 exclude = response.at( "exclude_from_board" );
225 aFetchedPart.exclude_from_board = boolFromString( exclude, false );
226 }
227
228 // initially assume no exclusion
229 if( response.contains( "exclude_from_sim" ) )
230 {
231 // if key value doesn't exists default to false
232 exclude = response.at( "exclude_from_sim" );
233 aFetchedPart.exclude_from_sim = boolFromString( exclude, false );
234 }
235
236 // remove previously loaded fields
237 aFetchedPart.fields.clear();
238
239 // Extract available fields
240 for( const auto& field : response.at( "fields" ).items() )
241 {
242 bool visible = true;
243
244 // name of the field
245 key = field.key();
246
247 // this is a dict
248 auto& properties = field.value();
249
250 value = properties.at( "value" );
251
252 // check if user wants to display field in schematic
253 if( properties.contains( "visible" ) )
254 {
255 std::string vis = properties.at( "visible" );
256 visible = boolFromString( vis, true );
257 }
258
259 // Add field to fields list
260 aFetchedPart.fields.push_back( std::make_pair( key, std::make_tuple( value, visible ) ) );
261 }
262
263 if( response.contains( "description" ) )
264 aFetchedPart.desc = response.at( "description" );
265
266 if( response.contains( "keywords" ) )
267 aFetchedPart.keywords = response.at( "keywords" );
268
269 if( response.contains( "footprint_filters" ) )
270 {
271 nlohmann::json filters_json = response.at( "footprint_filters" );
272
273 if( filters_json.is_array() )
274 {
275 for( const auto& val : filters_json )
276 aFetchedPart.fp_filters.push_back( val );
277 }
278 else
279 {
280 aFetchedPart.fp_filters.push_back( filters_json );
281 }
282 }
283 }
284 catch( const std::exception& e )
285 {
286 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
287 e.what(), res );
288
289 wxLogTrace( traceHTTPLib, wxT( "SelectOne: Exception while fetching part: %s" ), m_lastError );
290
291 return false;
292 }
293
294 m_cachedParts[aFetchedPart.id] = aFetchedPart;
295
296 return true;
297}
298
299
300bool HTTP_LIB_CONNECTION::SelectAll( const HTTP_LIB_CATEGORY& aCategory, std::vector<HTTP_LIB_PART>& aParts )
301{
302 if( !IsValidEndpoint() )
303 {
304 wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) );
305 return false;
306 }
307
308 std::string res = "";
309
310 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
311
312 curl->SetURL( m_source.root_url + fmt::format( "parts/category/{}.json", aCategory.id ) );
313
314 try
315 {
316 curl->Perform();
317
318 res = curl->GetBuffer();
319
320 nlohmann::json response = nlohmann::json::parse( res );
321
322 for( nlohmann::json& item : response )
323 {
324 //PART result;
325 HTTP_LIB_PART part;
326
327 part.id = item.at( "id" );
328
329 if( item.contains( "description" ) )
330 {
331 // At this point we don't display anything so just set it to false
332 part.fields.push_back( std::make_pair( "description",
333 std::make_tuple( item.at( "description" ), false ) ) );
334 }
335
336 // API might not want to return an optional name.
337 if( item.contains( "name" ) )
338 part.name = item.at( "name" );
339 else
340 part.name = part.id;
341
342 std::string originalName = part.name;
343 UTF8 sanitizedPartName = LIB_ID::FixIllegalChars( part.name, false );
344 part.name = sanitizedPartName.c_str();
345
346 if( part.name != originalName )
347 m_cache.erase( originalName );
348
349 // add to cache
350 m_cache[part.name] = std::make_tuple( part.id, aCategory.id );
351
352 aParts.emplace_back( std::move( part ) );
353 }
354 }
355 catch( const std::exception& e )
356 {
357 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
358 e.what(), res );
359
360 wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts: %s" ), m_lastError );
361
362 return false;
363 }
364
365 return true;
366}
367
368
369bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr<KICAD_CURL_EASY>& aCurl )
370{
371 int statusCode = aCurl->GetResponseStatusCode();
372
373 if( statusCode != 200 )
374 {
375 m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n",
376 httpErrorCodeDescription( statusCode ) );
377 return false;
378 }
379
380 return true;
381}
382
383
384bool HTTP_LIB_CONNECTION::boolFromString( const std::any& aVal, bool aDefaultValue )
385{
386 try
387 {
388 wxString strval( std::any_cast<std::string>( aVal ).c_str(), wxConvUTF8 );
389
390 if( strval.IsEmpty() )
391 return aDefaultValue;
392
393 strval.MakeLower();
394
395 for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } )
396 {
397 if( strval.Matches( trueVal ) )
398 return true;
399 }
400
401 for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } )
402 {
403 if( strval.Matches( falseVal ) )
404 return false;
405 }
406 }
407 catch( const std::bad_any_cast& )
408 {
409 }
410
411 return aDefaultValue;
412}
413
414
416{
417 auto codeDescription =
418 []( uint16_t aCode ) -> wxString
419 {
420 switch( aCode )
421 {
422 case 100: return wxS( "Continue" );
423 case 101: return wxS( "Switching Protocols" );
424 case 102: return wxS( "Processing" );
425 case 103: return wxS( "Early Hints" );
426
427 case 200: return wxS( "OK" );
428 case 201: return wxS( "Created" );
429 case 203: return wxS( "Non-Authoritative Information" );
430 case 204: return wxS( "No Content" );
431 case 205: return wxS( "Reset Content" );
432 case 206: return wxS( "Partial Content" );
433 case 207: return wxS( "Multi-Status" );
434 case 208: return wxS( "Already Reported" );
435 case 226: return wxS( "IM Used" );
436
437 case 300: return wxS( "Multiple Choices" );
438 case 301: return wxS( "Moved Permanently" );
439 case 302: return wxS( "Found" );
440 case 303: return wxS( "See Other" );
441 case 304: return wxS( "Not Modified" );
442 case 305: return wxS( "Use Proxy (Deprecated)" );
443 case 306: return wxS( "Unused" );
444 case 307: return wxS( "Temporary Redirect" );
445 case 308: return wxS( "Permanent Redirect" );
446
447 case 400: return wxS( "Bad Request" );
448 case 401: return wxS( "Unauthorized" );
449 case 402: return wxS( "Payment Required (Experimental)" );
450 case 403: return wxS( "Forbidden" );
451 case 404: return wxS( "Not Found" );
452 case 405: return wxS( "Method Not Allowed" );
453 case 406: return wxS( "Not Acceptable" );
454 case 407: return wxS( "Proxy Authentication Required" );
455 case 408: return wxS( "Request Timeout" );
456 case 409: return wxS( "Conflict" );
457 case 410: return wxS( "Gone" );
458 case 411: return wxS( "Length Required" );
459 case 412: return wxS( "Payload Too Large" );
460 case 414: return wxS( "URI Too Long" );
461 case 415: return wxS( "Unsupported Media Type" );
462 case 416: return wxS( "Range Not Satisfiable" );
463 case 417: return wxS( "Expectation Failed" );
464 case 418: return wxS( "I'm a teapot" );
465 case 421: return wxS( "Misdirected Request" );
466 case 422: return wxS( "Unprocessable Content" );
467 case 423: return wxS( "Locked" );
468 case 424: return wxS( "Failed Dependency" );
469 case 425: return wxS( "Too Early (Experimental)" );
470 case 426: return wxS( "Upgrade Required" );
471 case 428: return wxS( "Precondition Required" );
472 case 429: return wxS( "Too Many Requests" );
473 case 431: return wxS( "Request Header Fields Too Large" );
474 case 451: return wxS( "Unavailable For Legal Reasons" );
475
476 case 500: return wxS( "Internal Server Error" );
477 case 501: return wxS( "Not Implemented" );
478 case 502: return wxS( "Bad Gateway" );
479 case 503: return wxS( "Service Unavailable" );
480 case 504: return wxS( "Gateway Timeout" );
481 case 505: return wxS( "HTTP Version Not Supported" );
482 case 506: return wxS( "Variant Also Negotiates" );
483 case 507: return wxS( "Insufficient Storage" );
484 case 508: return wxS( "Loop Detected" );
485 case 510: return wxS( "Not Extended" );
486 case 511: return wxS( "Network Authentication Required" );
487 default: return wxS( "Unknown" );
488 }
489 };
490
491 return wxString::Format( wxS( "%d: %s" ), aHttpCode, codeDescription( aHttpCode ) );
492}
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 boolFromString(const std::any &aVal, bool aDefaultValue=false)
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
An 8 bit string that is assuredly encoded in UTF8, and supplies special conversion support to and fro...
Definition utf8.h:72
const char * c_str() const
Definition utf8.h:109
#define _(s)
const char *const traceHTTPLib
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