feat: Implement POST form submission

Add full support for POST method form submissions alongside existing GET support.

Changes:
- Add HttpClient::post() method with configurable Content-Type
- Implement URL encoding for form data (RFC 3986 compliant)
- Update Browser::submit_form() to detect and handle both GET and POST methods
- Add proper form data encoding with special character handling
- Include Content-Type header for POST requests

Features:
- Automatic method detection from form's method attribute
- URL-encoded form data (application/x-www-form-urlencoded)
- Proper encoding of special characters (spaces, &, =, etc.)
- Status messages for form submission feedback
- History tracking for POST responses

Testing:
- Add test_post_form.html with GET, POST, and special character test cases
- Uses httpbin.org endpoints for validation

Resolves TODO at browser.cpp:238 - POST handling is now fully implemented.
This commit is contained in:
m1ngsama 2025-12-25 14:12:07 +08:00
parent 0ecedb1aed
commit dec72c678f
4 changed files with 194 additions and 14 deletions

View file

@ -5,6 +5,8 @@
#include <algorithm> #include <algorithm>
#include <sstream> #include <sstream>
#include <map> #include <map>
#include <cctype>
#include <cstdio>
class Browser::Impl { class Browser::Impl {
public: public:
@ -207,42 +209,98 @@ public:
} }
} }
// URL encode helper function
std::string url_encode(const std::string& value) {
std::string result;
for (unsigned char c : value) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
result += c;
} else if (c == ' ') {
result += '+';
} else {
char hex[4];
snprintf(hex, sizeof(hex), "%%%02X", c);
result += hex;
}
}
return result;
}
void submit_form(DomNode* button) { void submit_form(DomNode* button) {
status_message = "Submitting form..."; status_message = "Submitting form...";
// Simple GET implementation for now
// Find parent form
DomNode* form = button->parent; DomNode* form = button->parent;
while (form && form->element_type != ElementType::FORM) form = form->parent; while (form && form->element_type != ElementType::FORM) form = form->parent;
if (!form) { if (!form) {
status_message = "Error: Button not in a form"; status_message = "Error: Button not in a form";
return; return;
} }
// Collect data // Collect form data with URL encoding
std::string query_string; std::string form_data;
for (DomNode* field : current_tree.form_fields) { for (DomNode* field : current_tree.form_fields) {
// Check if field belongs to this form // Check if field belongs to this form
DomNode* p = field->parent; DomNode* p = field->parent;
bool is_child = false; bool is_child = false;
while(p) { if(p == form) { is_child = true; break; } p = p->parent; } while(p) { if(p == form) { is_child = true; break; } p = p->parent; }
if (is_child && !field->name.empty()) { if (is_child && !field->name.empty()) {
if (!query_string.empty()) query_string += "&"; if (!form_data.empty()) form_data += "&";
query_string += field->name + "=" + field->value; form_data += url_encode(field->name) + "=" + url_encode(field->value);
} }
} }
std::string target_url = form->action; std::string target_url = form->action;
if (target_url.empty()) target_url = current_url; if (target_url.empty()) target_url = current_url;
// TODO: Handle POST. For now, assume GET or append query string // Check form method (default to GET if not specified)
if (target_url.find('?') == std::string::npos) { std::string method = form->method;
target_url += "?" + query_string; std::transform(method.begin(), method.end(), method.begin(), ::toupper);
} else {
target_url += "&" + query_string;
}
load_page(target_url); if (method == "POST") {
// POST request
status_message = "Sending POST request...";
HttpResponse response = http_client.post(target_url, form_data);
if (!response.error_message.empty()) {
status_message = "Error: " + response.error_message;
return;
}
if (!response.is_success()) {
status_message = "Error: HTTP " + std::to_string(response.status_code);
return;
}
// Parse and render response
DocumentTree tree = html_parser.parse_tree(response.body, target_url);
current_tree = std::move(tree);
current_url = target_url;
rendered_lines = renderer.render_tree(current_tree, screen_width);
build_interactive_list();
scroll_pos = 0;
current_element_index = -1;
// Update history
if (history_pos < static_cast<int>(history.size()) - 1) {
history.erase(history.begin() + history_pos + 1, history.end());
}
history.push_back(current_url);
history_pos = history.size() - 1;
status_message = "Form submitted (POST)";
} else {
// GET request (default)
if (target_url.find('?') == std::string::npos) {
target_url += "?" + form_data;
} else {
target_url += "&" + form_data;
}
load_page(target_url);
status_message = "Form submitted (GET)";
}
} }
void draw_status_bar() { void draw_status_bar() {

View file

@ -117,6 +117,95 @@ HttpResponse HttpClient::fetch(const std::string& url) {
return response; return response;
} }
HttpResponse HttpClient::post(const std::string& url, const std::string& data,
const std::string& content_type) {
HttpResponse response;
response.status_code = 0;
if (!pImpl->curl) {
response.error_message = "CURL not initialized";
return response;
}
curl_easy_reset(pImpl->curl);
// Re-apply settings
curl_easy_setopt(pImpl->curl, CURLOPT_URL, url.c_str());
// Set write callback
std::string response_body;
curl_easy_setopt(pImpl->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(pImpl->curl, CURLOPT_WRITEDATA, &response_body);
// Set timeout
curl_easy_setopt(pImpl->curl, CURLOPT_TIMEOUT, pImpl->timeout);
curl_easy_setopt(pImpl->curl, CURLOPT_CONNECTTIMEOUT, 10L);
// Set user agent
curl_easy_setopt(pImpl->curl, CURLOPT_USERAGENT, pImpl->user_agent.c_str());
// Set redirect following
if (pImpl->follow_redirects) {
curl_easy_setopt(pImpl->curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(pImpl->curl, CURLOPT_MAXREDIRS, 10L);
}
// HTTPS support
curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(pImpl->curl, CURLOPT_SSL_VERIFYHOST, 2L);
// Cookie settings
if (!pImpl->cookie_file.empty()) {
curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, pImpl->cookie_file.c_str());
curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEJAR, pImpl->cookie_file.c_str());
} else {
curl_easy_setopt(pImpl->curl, CURLOPT_COOKIEFILE, "");
}
// Enable automatic decompression
curl_easy_setopt(pImpl->curl, CURLOPT_ACCEPT_ENCODING, "");
// Set POST method
curl_easy_setopt(pImpl->curl, CURLOPT_POST, 1L);
// Set POST data
curl_easy_setopt(pImpl->curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(pImpl->curl, CURLOPT_POSTFIELDSIZE, data.length());
// Set Content-Type header
struct curl_slist* headers = nullptr;
std::string content_type_header = "Content-Type: " + content_type;
headers = curl_slist_append(headers, content_type_header.c_str());
curl_easy_setopt(pImpl->curl, CURLOPT_HTTPHEADER, headers);
// Perform request
CURLcode res = curl_easy_perform(pImpl->curl);
// Clean up headers
curl_slist_free_all(headers);
if (res != CURLE_OK) {
response.error_message = curl_easy_strerror(res);
return response;
}
// Get response code
long http_code = 0;
curl_easy_getinfo(pImpl->curl, CURLINFO_RESPONSE_CODE, &http_code);
response.status_code = static_cast<int>(http_code);
// Get Content-Type
char* resp_content_type = nullptr;
curl_easy_getinfo(pImpl->curl, CURLINFO_CONTENT_TYPE, &resp_content_type);
if (resp_content_type) {
response.content_type = resp_content_type;
}
response.body = std::move(response_body);
return response;
}
void HttpClient::set_timeout(long timeout_seconds) { void HttpClient::set_timeout(long timeout_seconds) {
pImpl->timeout = timeout_seconds; pImpl->timeout = timeout_seconds;
} }

View file

@ -20,6 +20,8 @@ public:
~HttpClient(); ~HttpClient();
HttpResponse fetch(const std::string& url); HttpResponse fetch(const std::string& url);
HttpResponse post(const std::string& url, const std::string& data,
const std::string& content_type = "application/x-www-form-urlencoded");
void set_timeout(long timeout_seconds); void set_timeout(long timeout_seconds);
void set_user_agent(const std::string& user_agent); void set_user_agent(const std::string& user_agent);
void set_follow_redirects(bool follow); void set_follow_redirects(bool follow);

31
test_post_form.html Normal file
View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>POST Form Test</title>
</head>
<body>
<h1>Form Method Test</h1>
<h2>GET Form</h2>
<form action="https://httpbin.org/get" method="get">
<p>Name: <input type="text" name="name" value="John"></p>
<p>Email: <input type="text" name="email" value="john@example.com"></p>
<p><input type="submit" value="Submit GET"></p>
</form>
<h2>POST Form</h2>
<form action="https://httpbin.org/post" method="post">
<p>Username: <input type="text" name="username" value="testuser"></p>
<p>Password: <input type="password" name="password" value="secret123"></p>
<p>Message: <input type="text" name="message" value="Hello World"></p>
<p><input type="submit" value="Submit POST"></p>
</form>
<h2>Form with Special Characters</h2>
<form action="https://httpbin.org/post" method="post">
<p>Text: <input type="text" name="text" value="Hello & goodbye!"></p>
<p>Code: <input type="text" name="code" value="a=b&c=d"></p>
<p><input type="submit" value="Submit"></p>
</form>
</body>
</html>