aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/browser.cc136
-rw-r--r--src/net/http_client.cc31
-rw-r--r--src/render/renderer.cc33
3 files changed, 138 insertions, 62 deletions
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 0f6d9d2..e6c63d4 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -37,6 +37,13 @@ struct PageData
std::string title;
};
+struct HttpRequest
+{
+ std::string url {};
+ std::string method = "GET";
+ std::string body {};
+};
+
namespace {
std::string extract_title( const parser::DomNode& root_node )
@@ -79,13 +86,12 @@ std::string extract_title( const parser::DomNode& root_node )
return "";
}
-PageData load_page( std::string_view url )
+PageData load_page( const HttpRequest& req )
{
const net::HttpClient client;
- auto response_result = client.fetch( url );
+ auto response_result = client.fetch( req.url, req.method, req.body );
if ( !response_result.has_value() ) {
- return {
- .lines = { "[!] Failed to load: " + std::string( url ) }, .links = {}, .inputs = {}, .title = "Error" };
+ return { .lines = { "[!] Failed to load: " + req.url }, .links = {}, .inputs = {}, .title = "Error" };
}
const parser::HtmlLexer lexer;
@@ -150,7 +156,7 @@ void draw_view( const tui::Terminal& terminal,
}
std::string footer
- = " URL: " + std::string( url ) + " | [j/k]Scroll [/]Jump [f]Follow [i]Input [H/L]History [q]Quit ";
+ = " URL: " + std::string( url ) + " | [j/k]Scroll [/]Jump [f]Follow [i]Input [r]Refresh [H/L]History [q]Quit ";
if ( footer.size() < width ) {
footer.append( width - footer.size(), ' ' );
} else if ( footer.size() > width && width > 3 ) {
@@ -364,66 +370,96 @@ std::string get_terminal_input( const tui::Terminal& terminal, const std::string
return input;
}
+InputAction submit_form( const render::InputField& target_input,
+ const std::vector<render::InputField>& inputs,
+ const std::string& current_url,
+ std::vector<HttpRequest>& history,
+ std::size_t& history_idx )
+{
+ std::string body;
+ for ( const auto& in : inputs ) {
+ if ( in.action == target_input.action && !in.name.empty() && in.type != "submit" && in.type != "button" ) {
+ std::string encoded = in.value;
+ for ( char& ch : encoded ) {
+ if ( ch == ' ' ) {
+ ch = '+';
+ }
+ }
+ if ( !body.empty() ) {
+ body += "&";
+ }
+ body += in.name + "=" + encoded;
+ }
+ }
+ const std::string action
+ = target_input.action.empty() ? current_url : normalize_url( target_input.action, current_url );
+ HttpRequest next_req { .url = action, .method = "GET", .body = "" };
+ if ( target_input.method == "post" || target_input.method == "POST" ) {
+ next_req.method = "POST";
+ next_req.body = body;
+ } else {
+ const char sep = action.contains( '?' ) ? '&' : '?';
+ next_req.url += sep + body;
+ }
+ history.resize( history_idx + 1 );
+ history.push_back( next_req );
+ history_idx++;
+ return InputAction::Navigate;
+}
+
+void update_input_value( PageData& page_data, std::size_t index, const std::string& query )
+{
+ page_data.inputs[index - 1].value = query;
+ const std::string marker = "[I" + std::to_string( index ) + ":";
+ for ( auto& line : page_data.lines ) {
+ const std::size_t pos = line.find( marker );
+ if ( pos != std::string::npos ) {
+ const std::size_t end = line.find( "]", pos );
+ if ( end != std::string::npos ) {
+ line.replace( pos, end - pos + 1, "[I" + std::to_string( index ) + ":" + query + "]" );
+ }
+ }
+ }
+}
+
InputAction handle_form_input( const tui::Terminal& terminal,
- const PageData& page_data,
- std::string& current_url,
- std::vector<std::string>& history,
+ PageData& page_data,
+ const std::string& current_url,
+ std::vector<HttpRequest>& history,
std::size_t& history_idx )
{
const std::string num_input = get_terminal_input( terminal, "Select Input [#]:" );
if ( num_input.empty() ) {
return InputAction::None;
}
-
std::size_t index = 0;
try {
index = std::stoul( num_input );
} catch ( ... ) {
- (void)0;
- }
- if ( index == 0 || index > page_data.inputs.size() ) {
return InputAction::None;
}
-
- const std::string query = get_terminal_input( terminal, "Enter Text [I" + std::to_string( index ) + "]:" );
- if ( query.empty() ) {
+ if ( index == 0 || index > page_data.inputs.size() ) {
return InputAction::None;
}
const auto& target_input = page_data.inputs[index - 1];
- std::string encoded_query = query;
- for ( char& ch : encoded_query ) {
- if ( ch == ' ' ) {
- ch = '+';
- }
- }
-
- std::string action = target_input.action;
- if ( action.empty() ) {
- action = current_url;
- } else {
- action = normalize_url( action, current_url );
+ if ( target_input.type == "submit" || target_input.type == "button" ) {
+ return submit_form( target_input, page_data.inputs, current_url, history, history_idx );
}
- std::string param_name = target_input.name;
- if ( param_name.empty() ) {
- param_name = "q";
+ const std::string query = get_terminal_input( terminal, "Enter Text [I" + std::to_string( index ) + "]:" );
+ if ( query.empty() ) {
+ return InputAction::None;
}
-
- const char sep = action.contains( '?' ) ? '&' : '?';
- const std::string target_url = action + sep + param_name + "=" + encoded_query;
-
- history.resize( history_idx + 1 );
- history.push_back( target_url );
- history_idx++;
- return InputAction::Navigate;
+ update_input_value( page_data, index, query );
+ return InputAction::None;
}
InputAction process_key( char c,
const tui::Terminal& terminal,
- const PageData& page_data,
- std::string& current_url,
- std::vector<std::string>& history,
+ PageData& page_data,
+ std::string current_url,
+ std::vector<HttpRequest>& history,
std::size_t& history_idx, // NOLINT(bugprone-easily-swappable-parameters)
std::size_t& scroll_y ) // NOLINT(bugprone-easily-swappable-parameters)
{
@@ -447,12 +483,15 @@ InputAction process_key( char c,
history_idx++;
return InputAction::Navigate;
}
+ if ( c == 'r' ) {
+ return InputAction::Navigate;
+ }
if ( c == '/' ) {
const std::string next_url = get_url_input( terminal );
if ( !next_url.empty() ) {
history.resize( history_idx + 1 );
- history.push_back( next_url );
+ history.push_back( HttpRequest { .url = next_url, .method = "GET", .body = "" } );
history_idx++;
return InputAction::Navigate;
}
@@ -464,7 +503,7 @@ InputAction process_key( char c,
handle_follow_link( terminal, page_data.links, current_url, nav );
if ( nav && current_url != old_url ) {
history.resize( history_idx + 1 );
- history.push_back( current_url );
+ history.push_back( HttpRequest { .url = current_url, .method = "GET", .body = "" } );
history_idx++;
return InputAction::Navigate;
}
@@ -513,19 +552,20 @@ std::vector<std::string> wrap_lines( const std::vector<std::string>& original_li
int Browser::run( std::string_view initial_url )
{
- std::vector<std::string> history;
- history.emplace_back( initial_url );
+ std::vector<HttpRequest> history;
+ history.emplace_back( HttpRequest { .url = std::string( initial_url ), .method = "GET", .body = "" } );
std::size_t history_idx = 0;
const tui::Terminal terminal;
terminal.show_cursor( false );
while ( true ) {
- std::string current_url = history[history_idx];
+ const HttpRequest current_req = history[history_idx];
+ const std::string current_url = current_req.url;
terminal.clear_screen();
- std::cout << "-> Fetching: " << current_url << "...\r\n";
+ std::cout << "-> Fetching: " << current_req.url << "...\r\n";
std::cout.flush();
- auto page_data = load_page( current_url );
+ auto page_data = load_page( current_req );
auto [term_width, term_height] = terminal.get_size();
const std::size_t wrap_width = ( term_width > 1 ) ? ( term_width - 1 ) : 80;
diff --git a/src/net/http_client.cc b/src/net/http_client.cc
index 143e067..0d75a7f 100644
--- a/src/net/http_client.cc
+++ b/src/net/http_client.cc
@@ -25,6 +25,7 @@
#include <cstddef>
#include <format>
#include <string>
+#include <string_view>
namespace ricochet::net {
@@ -169,22 +170,31 @@ struct FetchResult
FetchResult do_network_request( const std::string& host,
const std::string& service, // NOLINT(bugprone-easily-swappable-parameters)
const std::string& path, // NOLINT(bugprone-easily-swappable-parameters)
- bool is_https )
+ bool is_https,
+ const std::string& method, // NOLINT(bugprone-easily-swappable-parameters)
+ const std::string& body ) // NOLINT(bugprone-easily-swappable-parameters)
{
FetchResult res;
try {
const Address server_addr( host, service );
- const std::string request
- = std::format( "GET {} HTTP/1.1\r\n"
+ std::string request
+ = std::format( "{} {} HTTP/1.1\r\n"
"Host: {}\r\n"
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
"Accept-Language: en-US,en;q=0.9\r\n"
"DNT: 1\r\n"
- "Connection: close\r\n\r\n",
+ "Connection: close\r\n",
+ method,
path,
host );
+ if ( !body.empty() ) {
+ request += std::format( "Content-Length: {}\r\n"
+ "Content-Type: application/x-www-form-urlencoded\r\n",
+ body.length() );
+ }
+ request += "\r\n" + body;
if ( is_https ) {
SSLSocket ssl_socket;
@@ -217,10 +227,13 @@ FetchResult do_network_request( const std::string& host,
} // namespace
-std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url ) const
+std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url, // NOLINT
+ std::string_view method,
+ std::string_view body ) const
{
- (void)this; // 完美绕过 static 方法警告,满足最严苛的 tidy 规则
std::string current_url( url );
+ std::string current_method( method );
+ std::string current_body( body );
for ( int redirect_count = 0; redirect_count < 5; ++redirect_count ) {
const auto parsed_url = parse_url( current_url );
@@ -232,7 +245,7 @@ std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url
const bool is_https = current_url.starts_with( "https://" );
const std::string service = is_https ? "https" : "http";
- const FetchResult net_res = do_network_request( host, service, path, is_https );
+ const FetchResult net_res = do_network_request( host, service, path, is_https, current_method, current_body );
if ( !net_res.error.empty() ) {
return std::unexpected( net_res.error );
}
@@ -258,6 +271,10 @@ std::expected<HttpResponse, std::string> HttpClient::fetch( std::string_view url
const std::string next_url = extract_redirect_url( raw_response, is_https, host );
if ( !next_url.empty() ) {
current_url = next_url;
+ if ( response.status_code == 301 || response.status_code == 302 || response.status_code == 303 ) {
+ current_method = "GET";
+ current_body.clear();
+ }
continue;
}
}
diff --git a/src/render/renderer.cc b/src/render/renderer.cc
index 8708206..4b152b7 100644
--- a/src/render/renderer.cc
+++ b/src/render/renderer.cc
@@ -154,6 +154,30 @@ bool is_heading_tag( std::string_view tag )
return tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4";
}
+void render_input_node( const parser::DomNode& node,
+ RenderResult& result,
+ const FormContext& ctx,
+ std::string& output )
+{
+ std::string type = extract_attr( node.tag_name, "type" );
+ const std::size_t space_pos = node.tag_name.find( ' ' );
+ const std::string base_tag
+ = ( space_pos == std::string::npos ) ? node.tag_name : node.tag_name.substr( 0, space_pos );
+ if ( base_tag == "button" && type.empty() ) {
+ type = "submit";
+ }
+ const std::string default_val = extract_attr( node.tag_name, "value" );
+ result.inputs.push_back( { extract_attr( node.tag_name, "name" ), ctx.action, ctx.method, default_val, type } );
+
+ std::string hint = build_input_hint( node.tag_name );
+ if ( base_tag == "button" ) {
+ hint = "Submit";
+ }
+ if ( hint != "hidden" ) {
+ output += " \033[7;33m[I" + std::to_string( result.inputs.size() ) + ":" + hint + "]\033[0m ";
+ }
+}
+
void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
RenderResult& result,
FormContext ctx )
@@ -179,7 +203,7 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
const bool is_header = is_heading_tag( base_tag );
const bool is_link = ( base_tag == "a" );
const bool is_list_item = ( base_tag == "li" );
- const bool is_input = ( base_tag == "input" || base_tag == "textarea" );
+ const bool is_input = ( base_tag == "input" || base_tag == "textarea" || base_tag == "button" );
if ( is_header ) {
output += "\n\n\033[1;31m";
@@ -188,12 +212,7 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
} else if ( is_list_item ) {
output += "\n • ";
} else if ( is_input ) {
- result.inputs.push_back( { extract_attr( node.tag_name, "name" ), ctx.action, ctx.method } );
- const std::string hint = build_input_hint( node.tag_name );
-
- if ( hint != "hidden" ) {
- output += " \033[7;33m[I" + std::to_string( result.inputs.size() ) + ":" + hint + "]\033[0m ";
- }
+ render_input_node( node, result, ctx, output );
} else if ( base_tag == "p" || base_tag == "div" || is_header ) {
if ( !output.empty() && output.back() != '\n' ) {
output += "\n";