You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

501 lines
16 KiB

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