/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2023 Andre F. K. Iwers * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include const char* const traceHTTPLib = "KICAD_HTTP_LIB"; HTTP_LIB_CONNECTION::HTTP_LIB_CONNECTION( const HTTP_LIB_SOURCE& aSource, bool aTestConnectionNow ) { m_source = aSource; if( aTestConnectionNow ) { ValidateHTTPLibraryEndpoints(); } } HTTP_LIB_CONNECTION::~HTTP_LIB_CONNECTION() { // Do nothing } bool HTTP_LIB_CONNECTION::ValidateHTTPLibraryEndpoints() { m_endpointValid = false; std::string res = ""; std::unique_ptr curl = createCurlEasyObject(); curl->SetURL( m_source.root_url ); try { curl->Perform(); res = curl->GetBuffer(); if( !checkServerResponse( curl ) ) return false; if( res.length() == 0 ) { m_lastError += wxString::Format( _( "KiCad received an empty response!" ) + "\n" ); } else { nlohmann::json response = nlohmann::json::parse( res ); // Check that the endpoints exist, if not fail. if( !response.at( http_endpoint_categories ).empty() && !response.at( http_endpoint_parts ).empty() ) { m_endpointValid = true; } } } catch( const std::exception& e ) { m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", e.what(), res ); wxLogTrace( traceHTTPLib, wxT( "ValidateHTTPLibraryEndpoints: Exception occurred while testing the API " "connection: %s" ), m_lastError ); m_endpointValid = false; } if( m_endpointValid ) { syncCategories(); } return m_endpointValid; } bool HTTP_LIB_CONNECTION::IsValidEndpoint() const { return m_endpointValid; } bool HTTP_LIB_CONNECTION::syncCategories() { if( !IsValidEndpoint() ) { wxLogTrace( traceHTTPLib, wxT( "syncCategories: without valid connection!" ) ); return false; } std::string res = ""; std::unique_ptr curl = createCurlEasyObject(); curl->SetURL( m_source.root_url + http_endpoint_categories + ".json" ); try { curl->Perform(); res = curl->GetBuffer(); if( !checkServerResponse( curl ) ) { return false; } nlohmann::json response = nlohmann::json::parse( res ); // collect the categories in vector for( const auto& item : response.items() ) { HTTP_LIB_CATEGORY category; category.id = item.value()["id"].get(); category.name = item.value()["name"].get(); m_categories.push_back( category ); } } catch( const std::exception& e ) { m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", e.what(), res ); wxLogTrace( traceHTTPLib, wxT( "syncCategories: Exception occurred while syncing categories: %s" ), m_lastError ); m_categories.clear(); return false; } return true; } bool HTTP_LIB_CONNECTION::SelectOne( const std::string& aPartID, HTTP_LIB_PART& aFetchedPart ) { if( !IsValidEndpoint() ) { wxLogTrace( traceHTTPLib, wxT( "SelectOne: without valid connection!" ) ); return false; } // Check if there is already a part in our cache, if not fetch it if( m_cachedParts.find( aPartID ) != m_cachedParts.end() ) { // check if it's outdated, if so re-fetch if( std::difftime( std::time( nullptr ), m_cachedParts[aPartID].lastCached ) < m_source.timeout_parts ) { aFetchedPart = m_cachedParts[aPartID]; return true; } } std::string res = ""; std::unique_ptr curl = createCurlEasyObject(); std::string url = m_source.root_url + fmt::format( "{}/{}.json", http_endpoint_parts, aPartID ); curl->SetURL( url ); try { curl->Perform(); res = curl->GetBuffer(); if( !checkServerResponse( curl ) ) { return false; } nlohmann::json response = nlohmann::json::parse( res ); std::string key = ""; std::string value = ""; // the id used to identify the part, the name is needed to show a human-readable // part description to the user inside the symbol chooser dialog aFetchedPart.id = response.at( "id" ); // get a timestamp for caching aFetchedPart.lastCached = std::time( nullptr ); // API might not want to return an optional name. if( response.contains( "name" ) ) { aFetchedPart.name = response.at( "name" ); } else { aFetchedPart.name = aFetchedPart.id; } aFetchedPart.symbolIdStr = response.at( "symbolIdStr" ); // initially assume no exclusion std::string exclude; if( response.contains( "exclude_from_bom" ) ) { // if key value doesn't exists default to false exclude = response.at( "exclude_from_bom" ); aFetchedPart.exclude_from_bom = boolFromString( exclude, false ); } // initially assume no exclusion if( response.contains( "exclude_from_board" ) ) { // if key value doesn't exists default to false exclude = response.at( "exclude_from_board" ); aFetchedPart.exclude_from_board = boolFromString( exclude, false ); } // initially assume no exclusion if( response.contains( "exclude_from_sim" ) ) { // if key value doesn't exists default to false exclude = response.at( "exclude_from_sim" ); aFetchedPart.exclude_from_sim = boolFromString( exclude, false ); } // Extract available fields for( const auto& field : response.at( "fields" ).items() ) { bool visible = true; // name of the field key = field.key(); // this is a dict auto& properties = field.value(); value = properties.at( "value" ); // check if user wants to display field in schematic if( properties.contains( "visible" ) ) { std::string vis = properties.at( "visible" ); visible = boolFromString( vis, true ); } // Add field to fields list if( key.length() ) { aFetchedPart.fields[key] = std::make_tuple( value, visible ); } } } catch( const std::exception& e ) { m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", e.what(), res ); wxLogTrace( traceHTTPLib, wxT( "SelectOne: Exception occurred while retrieving part from REST API: %s" ), m_lastError ); return false; } m_cachedParts[aFetchedPart.id] = aFetchedPart; return true; } bool HTTP_LIB_CONNECTION::SelectAll( const HTTP_LIB_CATEGORY& aCategory, std::vector& aParts ) { if( !IsValidEndpoint() ) { wxLogTrace( traceHTTPLib, wxT( "SelectAll: without valid connection!" ) ); return false; } std::string res = ""; std::unique_ptr curl = createCurlEasyObject(); curl->SetURL( m_source.root_url + fmt::format( "{}/category/{}.json", http_endpoint_parts, aCategory.id ) ); try { curl->Perform(); res = curl->GetBuffer(); nlohmann::json response = nlohmann::json::parse( res ); std::string key = ""; std::string value = ""; for( nlohmann::json& item : response ) { //PART result; HTTP_LIB_PART part; part.id = item.at( "id" ); if( item.contains( "description" ) ) { // At this point we don't display anything so just set it to false part.fields["description"] = std::make_tuple( item.at( "description" ), false ); } // API might not want to return an optional name. if( item.contains( "name" ) ) { part.name = item.at( "name" ); } else { part.name = part.id; } // add to cache m_cache[part.name] = std::make_tuple( part.id, aCategory.id ); aParts.emplace_back( std::move( part ) ); } } catch( const std::exception& e ) { m_lastError += wxString::Format( _( "Error: %s" ) + "\n" + _( "API Response: %s" ) + "\n", e.what(), res ); wxLogTrace( traceHTTPLib, wxT( "Exception occurred while syncing parts from REST API: %s" ), m_lastError ); return false; } return true; } bool HTTP_LIB_CONNECTION::checkServerResponse( std::unique_ptr& aCurl ) { int statusCode = aCurl->GetResponseStatusCode(); if( statusCode != 200 ) { m_lastError += wxString::Format( _( "API responded with error code: %s" ) + "\n", httpErrorCodeDescription( statusCode ) ); return false; } return true; } bool HTTP_LIB_CONNECTION::boolFromString( const std::any& aVal, bool aDefaultValue ) { try { wxString strval( std::any_cast( aVal ).c_str(), wxConvUTF8 ); if( strval.IsEmpty() ) return aDefaultValue; strval.MakeLower(); for( const auto& trueVal : { wxS( "true" ), wxS( "yes" ), wxS( "y" ), wxS( "1" ) } ) { if( strval.Matches( trueVal ) ) return true; } for( const auto& falseVal : { wxS( "false" ), wxS( "no" ), wxS( "n" ), wxS( "0" ) } ) { if( strval.Matches( falseVal ) ) return false; } } catch( const std::bad_any_cast& ) { } return aDefaultValue; } /* * HTTP response status codes indicate whether a specific HTTP request has been successfully completed. * Responses are grouped in five classes: * Informational responses (100 ? 199) * Successful responses (200 ? 299) * Redirection messages (300 ? 399) * Client error responses (400 ? 499) * Server error responses (500 ? 599) * * see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status */ wxString HTTP_LIB_CONNECTION::httpErrorCodeDescription( uint16_t aHttpCode ) { auto codeDescription = []( uint16_t aCode ) -> wxString { switch( aCode ) { case 100: return wxS( "Continue" ); case 101: return wxS( "Switching Protocols" ); case 102: return wxS( "Processing" ); case 103: return wxS( "Early Hints" ); case 200: return wxS( "OK" ); case 201: return wxS( "Created" ); case 203: return wxS( "Non-Authoritative Information" ); case 204: return wxS( "No Content" ); case 205: return wxS( "Reset Content" ); case 206: return wxS( "Partial Content" ); case 207: return wxS( "Multi-Status" ); case 208: return wxS( "Already Reporte" ); case 226: return wxS( "IM Used" ); case 300: return wxS( "Multiple Choices" ); case 301: return wxS( "Moved Permanently" ); case 302: return wxS( "Found" ); case 303: return wxS( "See Other" ); case 304: return wxS( "Not Modified" ); case 305: return wxS( "Use Proxy (Deprecated)" ); case 306: return wxS( "Unused" ); case 307: return wxS( "Temporary Redirect" ); case 308: return wxS( "Permanent Redirect" ); case 400: return wxS( "Bad Request" ); case 401: return wxS( "Unauthorized" ); case 402: return wxS( "Payment Required (Experimental)" ); case 403: return wxS( "Forbidden" ); case 404: return wxS( "Not Found" ); case 405: return wxS( "Method Not Allowed" ); case 406: return wxS( "Not Acceptable" ); case 407: return wxS( "Proxy Authentication Required" ); case 408: return wxS( "Request Timeout" ); case 409: return wxS( "Conflict" ); case 410: return wxS( "Gone" ); case 411: return wxS( "Length Required" ); case 412: return wxS( "Payload Too Large" ); case 414: return wxS( "URI Too Long" ); case 415: return wxS( "Unsupported Media Type" ); case 416: return wxS( "Range Not Satisfiable" ); case 417: return wxS( "Expectation Failed" ); case 418: return wxS( "I'm a teapot" ); case 421: return wxS( "Misdirected Request" ); case 422: return wxS( "Unprocessable Conten" ); case 423: return wxS( "Locked" ); case 424: return wxS( "Failed Dependency" ); case 425: return wxS( "Too Early (Experimental)" ); case 426: return wxS( "Upgrade Required" ); case 428: return wxS( "Precondition Required" ); case 429: return wxS( "Too Many Requests" ); case 431: return wxS( "Request Header Fields Too Large" ); case 451: return wxS( "Unavailable For Legal Reasons" ); case 500: return wxS( "Internal Server Error" ); case 501: return wxS( "Not Implemented" ); case 502: return wxS( "Bad Gateway" ); case 503: return wxS( "Service Unavailable" ); case 504: return wxS( "Gateway Timeout" ); case 505: return wxS( "HTTP Version Not Supported" ); case 506: return wxS( "Variant Also Negotiates" ); case 507: return wxS( "Insufficient Storag" ); case 508: return wxS( "Loop Detecte" ); case 510: return wxS( "Not Extended" ); case 511: return wxS( "Network Authentication Required" ); default: return wxS( "Unknown" ); } }; return wxString::Format( wxS( "%d: %s" ), aHttpCode, codeDescription( aHttpCode ) ); }