mirror of
https://github.com/m1ngsama/TUT.git
synced 2025-12-26 12:04:11 +00:00
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:
parent
0ecedb1aed
commit
dec72c678f
4 changed files with 194 additions and 14 deletions
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
31
test_post_form.html
Normal 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>
|
||||||
Loading…
Reference in a new issue