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 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <wx/log.h>
21#include <fmt/core.h>
22#include <wx/translation.h>
23#include <ctime>
24
25#include <boost/algorithm/string.hpp>
26#include <nlohmann/json.hpp>
27#include <wx/base64.h>
28
30#include <curl/curl.h>
31
33
34const char* const traceHTTPLib = "KICAD_HTTP_LIB";
35
36
37HTTP_LIB_CONNECTION::HTTP_LIB_CONNECTION( const HTTP_LIB_SOURCE& aSource, bool aTestConnectionNow )
38{
39 m_source = aSource;
40
41 if( aTestConnectionNow )
42 {
44 }
45}
46
47
49{
50 // Do nothing
51}
52
53
55{
56 m_endpointValid = false;
57 std::string res = "";
58
59 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
60 curl->SetURL( m_source.root_url );
61
62 try
63 {
64 curl->Perform();
65
66 res = curl->GetBuffer();
67
68 if( !checkServerResponse( curl ) )
69 return false;
70
71 if( res.length() == 0 )
72 {
73 m_lastError += wxString::Format( _( "KiCad received an empty response!" ) + "\n" );
74 }
75 else
76 {
77 nlohmann::json response = nlohmann::json::parse( res );
78
79 // Check that the endpoints exist, if not fail.
80 if( !response.at( http_endpoint_categories ).empty()
81 && !response.at( http_endpoint_parts ).empty() )
82 {
83 m_endpointValid = true;
84 }
85 }
86 }
87 catch( const std::exception& e )
88 {
89 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
90 e.what(), res );
91
92 wxLogTrace( traceHTTPLib,
93 wxT( "ValidateHTTPLibraryEndpoints: Exception occurred while testing the API "
94 "connection: %s" ),
96
97 m_endpointValid = false;
98 }
99
100 if( m_endpointValid )
101 {
103 }
104
105 return m_endpointValid;
106}
107
108
110{
111 return m_endpointValid;
112}
113
114
116{
117 if( !IsValidEndpoint() )
118 {
119 wxLogTrace( traceHTTPLib, wxT( "syncCategories: without valid connection!" ) );
120 return false;
121 }
122
123 std::string res = "";
124
125 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
126 curl->SetURL( m_source.root_url + http_endpoint_categories + ".json" );
127
128 try
129 {
130 curl->Perform();
131
132 res = curl->GetBuffer();
133
134 if( !checkServerResponse( curl ) )
135 {
136 return false;
137 }
138
139 nlohmann::json response = nlohmann::json::parse( res );
140
141 // collect the categories in vector
142 for( const auto& item : response.items() )
143 {
144 HTTP_LIB_CATEGORY category;
145
146 auto& value = item.value();
147 category.id = value["id"].get<std::string>();
148 category.name = value["name"].get<std::string>();
149
150 if( value.contains( "description" ) )
151 {
152 category.description = value["description"].get<std::string>();
153 m_categoryDescriptions[category.name] = category.description;
154 }
155
156 m_categories.push_back( category );
157 }
158 }
159 catch( const std::exception& e )
160 {
161 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
162 e.what(), res );
163
164 wxLogTrace( traceHTTPLib,
165 wxT( "syncCategories: Exception occurred while syncing categories: %s" ),
166 m_lastError );
167
168 m_categories.clear();
169
170 return false;
171 }
172
173 return true;
174}
175
176
177bool HTTP_LIB_CONNECTION::SelectOne( const std::string& aPartID, HTTP_LIB_PART& aFetchedPart )
178{
179 if( !IsValidEndpoint() )
180 {
181 wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) );
182 return false;
183 }
184
185 // Check if there is already a part in our cache, if not fetch it
186 if( m_cachedParts.find( aPartID ) != m_cachedParts.end() )
187 {
188 // check if it's outdated, if so re-fetch
189 if( std::difftime( std::time( nullptr ), m_cachedParts[aPartID].lastCached )
191 {
192 aFetchedPart = m_cachedParts[aPartID];
193 return true;
194 }
195 }
196
197 std::string res = "";
198
199 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
200 std::string url = m_source.root_url + fmt::format( "{}/{}.json", http_endpoint_parts, aPartID );
201 curl->SetURL( url );
202
203 try
204 {
205 curl->Perform();
206
207 res = curl->GetBuffer();
208
209 if( !checkServerResponse( curl ) )
210 {
211 return false;
212 }
213
214 nlohmann::ordered_json response = nlohmann::ordered_json::parse( res );
215 std::string key = "";
216 std::string value = "";
217
218 // the id used to identify the part, the name is needed to show a human-readable
219 // part description to the user inside the symbol chooser dialog
220 aFetchedPart.id = response.at( "id" );
221
222 // get a timestamp for caching
223 aFetchedPart.lastCached = std::time( nullptr );
224
225 // API might not want to return an optional name.
226 if( response.contains( "name" ) )
227 {
228 aFetchedPart.name = response.at( "name" );
229 }
230 else
231 {
232 aFetchedPart.name = aFetchedPart.id;
233 }
234
235 aFetchedPart.symbolIdStr = response.at( "symbolIdStr" );
236
237 // initially assume no exclusion
238 std::string exclude;
239
240 if( response.contains( "exclude_from_bom" ) )
241 {
242 // if key value doesn't exists default to false
243 exclude = response.at( "exclude_from_bom" );
244 aFetchedPart.exclude_from_bom = boolFromString( exclude, false );
245 }
246
247 // initially assume no exclusion
248 if( response.contains( "exclude_from_board" ) )
249 {
250 // if key value doesn't exists default to false
251 exclude = response.at( "exclude_from_board" );
252 aFetchedPart.exclude_from_board = boolFromString( exclude, false );
253 }
254
255 // initially assume no exclusion
256 if( response.contains( "exclude_from_sim" ) )
257 {
258 // if key value doesn't exists default to false
259 exclude = response.at( "exclude_from_sim" );
260 aFetchedPart.exclude_from_sim = boolFromString( exclude, false );
261 }
262
263 // remove previously loaded fields
264 aFetchedPart.fields.clear();
265
266 // Extract available fields
267 for( const auto& field : response.at( "fields" ).items() )
268 {
269 bool visible = true;
270
271 // name of the field
272 key = field.key();
273
274 // this is a dict
275 auto& properties = field.value();
276
277 value = properties.at( "value" );
278
279 // check if user wants to display field in schematic
280 if( properties.contains( "visible" ) )
281 {
282 std::string vis = properties.at( "visible" );
283 visible = boolFromString( vis, true );
284 }
285
286 // Add field to fields list
287 aFetchedPart.fields.push_back(
288 std::make_pair( key, std::make_tuple( value, visible ) ) );
289 }
290 }
291 catch( const std::exception& e )
292 {
293 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
294 e.what(), res );
295
296 wxLogTrace( traceHTTPLib,
297 wxT( "SelectOne: Exception occurred while retrieving part from REST API: %s" ),
298 m_lastError );
299
300 return false;
301 }
302
303 m_cachedParts[aFetchedPart.id] = aFetchedPart;
304
305 return true;
306}
307
308
310 std::vector<HTTP_LIB_PART>& aParts )
311{
312 if( !IsValidEndpoint() )
313 {
314 wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) );
315 return false;
316 }
317
318 std::string res = "";
319
320 std::unique_ptr<KICAD_CURL_EASY> curl = createCurlEasyObject();
321
322 curl->SetURL( m_source.root_url
323 + fmt::format( "{}/category/{}.json", http_endpoint_parts, aCategory.id ) );
324
325 try
326 {
327 curl->Perform();
328
329 res = curl->GetBuffer();
330
331 nlohmann::json response = nlohmann::json::parse( res );
332
333 for( nlohmann::json& item : response )
334 {
335 //PART result;
336 HTTP_LIB_PART part;
337
338 part.id = item.at( "id" );
339
340 if( item.contains( "description" ) )
341 {
342 // At this point we don't display anything so just set it to false
343 part.fields.push_back( std::make_pair(
344 "description", std::make_tuple( item.at( "description" ), false ) ) );
345 }
346
347 // API might not want to return an optional name.
348 if( item.contains( "name" ) )
349 {
350 part.name = item.at( "name" );
351 }
352 else
353 {
354 part.name = part.id;
355 }
356
357 // add to cache
358 m_cache[part.name] = std::make_tuple( part.id, aCategory.id );
359
360 aParts.emplace_back( std::move( part ) );
361 }
362 }
363 catch( const std::exception& e )
364 {
365 m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n",
366 e.what(), res );
367
368 wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts from REST API: %s" ),
369 m_lastError );
370
371 return false;
372 }
373
374 return true;
375}
376
377
378bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr<KICAD_CURL_EASY>& aCurl )
379{
380 int statusCode = aCurl->GetResponseStatusCode();
381 if( statusCode != 200 )
382 {
383 m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n",
384 httpErrorCodeDescription( statusCode ) );
385 return false;
386 }
387
388 return true;
389}
390
391
392bool HTTP_LIB_CONNECTION::boolFromString( const std::any& aVal, bool aDefaultValue )
393{
394 try
395 {
396 wxString strval( std::any_cast<std::string>( aVal ).c_str(), wxConvUTF8 );
397
398 if( strval.IsEmpty() )
399 return aDefaultValue;
400
401 strval.MakeLower();
402
403 for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } )
404 {
405 if( strval.Matches( trueVal ) )
406 return true;
407 }
408
409 for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } )
410 {
411 if( strval.Matches( falseVal ) )
412 return false;
413 }
414 }
415 catch( const std::bad_any_cast& )
416 {
417 }
418
419 return aDefaultValue;
420}
421
422/*
423* HTTP response status codes indicate whether a specific HTTP request has been successfully completed.
424* Responses are grouped in five classes:
425* Informational responses (100 ? 199)
426* Successful responses (200 ? 299)
427* Redirection messages (300 ? 399)
428* Client error responses (400 ? 499)
429* Server error responses (500 ? 599)
430*
431* see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
432*/
434{
435 auto codeDescription =
436 []( uint16_t aCode ) -> wxString
437 {
438 switch( aCode )
439 {
440 case 100: return wxS( "Continue" );
441 case 101: return wxS( "Switching Protocols" );
442 case 102: return wxS( "Processing" );
443 case 103: return wxS( "Early Hints" );
444
445 case 200: return wxS( "OK" );
446 case 201: return wxS( "Created" );
447 case 203: return wxS( "Non-Authoritative Information" );
448 case 204: return wxS( "No Content" );
449 case 205: return wxS( "Reset Content" );
450 case 206: return wxS( "Partial Content" );
451 case 207: return wxS( "Multi-Status" );
452 case 208: return wxS( "Already Reporte" );
453 case 226: return wxS( "IM Used" );
454
455 case 300: return wxS( "Multiple Choices" );
456 case 301: return wxS( "Moved Permanently" );
457 case 302: return wxS( "Found" );
458 case 303: return wxS( "See Other" );
459 case 304: return wxS( "Not Modified" );
460 case 305: return wxS( "Use Proxy (Deprecated)" );
461 case 306: return wxS( "Unused" );
462 case 307: return wxS( "Temporary Redirect" );
463 case 308: return wxS( "Permanent Redirect" );
464
465 case 400: return wxS( "Bad Request" );
466 case 401: return wxS( "Unauthorized" );
467 case 402: return wxS( "Payment Required (Experimental)" );
468 case 403: return wxS( "Forbidden" );
469 case 404: return wxS( "Not Found" );
470 case 405: return wxS( "Method Not Allowed" );
471 case 406: return wxS( "Not Acceptable" );
472 case 407: return wxS( "Proxy Authentication Required" );
473 case 408: return wxS( "Request Timeout" );
474 case 409: return wxS( "Conflict" );
475 case 410: return wxS( "Gone" );
476 case 411: return wxS( "Length Required" );
477 case 412: return wxS( "Payload Too Large" );
478 case 414: return wxS( "URI Too Long" );
479 case 415: return wxS( "Unsupported Media Type" );
480 case 416: return wxS( "Range Not Satisfiable" );
481 case 417: return wxS( "Expectation Failed" );
482 case 418: return wxS( "I'm a teapot" );
483 case 421: return wxS( "Misdirected Request" );
484 case 422: return wxS( "Unprocessable Conten" );
485 case 423: return wxS( "Locked" );
486 case 424: return wxS( "Failed Dependency" );
487 case 425: return wxS( "Too Early (Experimental)" );
488 case 426: return wxS( "Upgrade Required" );
489 case 428: return wxS( "Precondition Required" );
490 case 429: return wxS( "Too Many Requests" );
491 case 431: return wxS( "Request Header Fields Too Large" );
492 case 451: return wxS( "Unavailable For Legal Reasons" );
493
494 case 500: return wxS( "Internal Server Error" );
495 case 501: return wxS( "Not Implemented" );
496 case 502: return wxS( "Bad Gateway" );
497 case 503: return wxS( "Service Unavailable" );
498 case 504: return wxS( "Gateway Timeout" );
499 case 505: return wxS( "HTTP Version Not Supported" );
500 case 506: return wxS( "Variant Also Negotiates" );
501 case 507: return wxS( "Insufficient Storag" );
502 case 508: return wxS( "Loop Detecte" );
503 case 510: return wxS( "Not Extended" );
504 case 511: return wxS( "Network Authentication Required" );
505 default: return wxS( "Unknown" );
506 }
507 };
508
509 return wxString::Format( wxS( "%d: %s" ), aHttpCode, codeDescription( aHttpCode ) );
510}
const std::string http_endpoint_parts
bool checkServerResponse(std::unique_ptr< KICAD_CURL_EASY > &aCurl)
const std::string http_endpoint_categories
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)
Retrieves 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
HTTP_LIB_SOURCE m_source
bool SelectOne(const std::string &aPartID, HTTP_LIB_PART &aFetchedPart)
Retrieves a single part with full details from the HTTP library.
wxString httpErrorCodeDescription(uint16_t aHttpCode)
#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 name
std::string symbolIdStr
std::string id
std::time_t lastCached
std::vector< std::pair< std::string, std::tuple< std::string, bool > > > fields
additional generic fields
std::string root_url
VECTOR3I res