Line data Source code
1 : #include "form.hpp"
2 : #include "../server/mime_types.hpp"
3 : #include "../util/url.hpp"
4 : #include <fstream>
5 : #include <sstream>
6 : #include <random>
7 : #include <algorithm>
8 :
9 : namespace thinger::http {
10 :
11 : // ============================================
12 : // Field methods
13 : // ============================================
14 :
15 22 : form& form::field(std::string name, std::string value) {
16 22 : fields_.emplace_back(std::move(name), std::move(value));
17 22 : return *this;
18 : }
19 :
20 2 : form& form::fields(std::initializer_list<std::pair<std::string, std::string>> pairs) {
21 8 : for (const auto& [name, value] : pairs) {
22 6 : fields_.emplace_back(name, value);
23 : }
24 2 : return *this;
25 : }
26 :
27 : // ============================================
28 : // File methods
29 : // ============================================
30 :
31 0 : form& form::file(std::string name, const std::filesystem::path& path) {
32 0 : return file(std::move(name), path, mime_type(path));
33 : }
34 :
35 0 : form& form::file(std::string name, const std::filesystem::path& path,
36 : std::string content_type) {
37 : // Read file content
38 0 : std::ifstream ifs(path, std::ios::binary);
39 0 : if (!ifs) {
40 0 : return *this;
41 : }
42 :
43 0 : std::ostringstream oss;
44 0 : oss << ifs.rdbuf();
45 :
46 0 : files_.push_back({
47 0 : std::move(name),
48 0 : path.filename().string(),
49 0 : std::move(content_type),
50 : oss.str()
51 : });
52 :
53 0 : return *this;
54 0 : }
55 :
56 6 : form& form::file(std::string name, std::string content,
57 : std::string filename, std::string content_type) {
58 6 : files_.push_back({
59 6 : std::move(name),
60 6 : std::move(filename),
61 6 : std::move(content_type),
62 6 : std::move(content)
63 : });
64 6 : return *this;
65 6 : }
66 :
67 2 : form& form::file(std::string name, const void* data, size_t size,
68 : std::string filename, std::string content_type) {
69 2 : files_.push_back({
70 2 : std::move(name),
71 2 : std::move(filename),
72 2 : std::move(content_type),
73 : std::string(static_cast<const char*>(data), size)
74 : });
75 2 : return *this;
76 6 : }
77 :
78 : // ============================================
79 : // Content-Type and body generation
80 : // ============================================
81 :
82 10 : std::string form::content_type() const {
83 10 : if (is_multipart()) {
84 4 : return "multipart/form-data; boundary=" + get_boundary();
85 : }
86 12 : return "application/x-www-form-urlencoded";
87 : }
88 :
89 16 : std::string form::body() const {
90 16 : if (is_multipart()) {
91 4 : return build_multipart();
92 : }
93 12 : return build_urlencoded();
94 : }
95 :
96 12 : std::string form::build_urlencoded() const {
97 12 : std::string result;
98 34 : for (const auto& [name, value] : fields_) {
99 22 : if (!result.empty()) {
100 10 : result += '&';
101 : }
102 22 : result += url_encode(name) + '=' + url_encode(value);
103 : }
104 12 : return result;
105 0 : }
106 :
107 4 : std::string form::build_multipart() const {
108 4 : const std::string& boundary = get_boundary();
109 4 : std::ostringstream oss;
110 :
111 : // Add text fields
112 6 : for (const auto& [name, value] : fields_) {
113 2 : oss << "--" << boundary << "\r\n";
114 2 : oss << "Content-Disposition: form-data; name=\"" << name << "\"\r\n";
115 2 : oss << "\r\n";
116 2 : oss << value << "\r\n";
117 : }
118 :
119 : // Add files
120 8 : for (const auto& file : files_) {
121 4 : oss << "--" << boundary << "\r\n";
122 4 : oss << "Content-Disposition: form-data; name=\"" << file.name << "\"; "
123 4 : << "filename=\"" << file.filename << "\"\r\n";
124 4 : oss << "Content-Type: " << file.content_type << "\r\n";
125 4 : oss << "\r\n";
126 4 : oss << file.content << "\r\n";
127 : }
128 :
129 : // Closing boundary
130 4 : oss << "--" << boundary << "--\r\n";
131 :
132 8 : return oss.str();
133 4 : }
134 :
135 8 : const std::string& form::get_boundary() const {
136 8 : if (boundary_.empty()) {
137 6 : boundary_ = generate_boundary();
138 : }
139 8 : return boundary_;
140 : }
141 :
142 6 : std::string form::generate_boundary() {
143 : static const char chars[] =
144 : "0123456789"
145 : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
146 : "abcdefghijklmnopqrstuvwxyz";
147 :
148 6 : std::random_device rd;
149 6 : std::mt19937 gen(rd());
150 6 : std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
151 :
152 6 : std::string boundary = "----ThingerFormBoundary";
153 102 : for (int i = 0; i < 16; ++i) {
154 96 : boundary += chars[dis(gen)];
155 : }
156 :
157 12 : return boundary;
158 6 : }
159 :
160 : // ============================================
161 : // URL encoding/decoding (delegates to util::url)
162 : // ============================================
163 :
164 58 : std::string form::url_encode(const std::string& str) {
165 : // application/x-www-form-urlencoded uses '+' for space instead of '%20'
166 : static constexpr char hex[] = "0123456789ABCDEF";
167 58 : std::string result;
168 58 : result.reserve(str.size());
169 412 : for (unsigned char c : str) {
170 354 : if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
171 314 : result += static_cast<char>(c);
172 40 : } else if (c == ' ') {
173 4 : result += '+';
174 : } else {
175 36 : result += '%';
176 36 : result += hex[c >> 4];
177 36 : result += hex[c & 0x0F];
178 : }
179 : }
180 58 : return result;
181 0 : }
182 :
183 6 : std::string form::url_decode(const std::string& str) {
184 6 : return util::url::url_decode(str);
185 : }
186 :
187 : // ============================================
188 : // MIME type detection
189 : // ============================================
190 :
191 30 : std::string form::mime_type(const std::filesystem::path& path) {
192 : // Use the library's comprehensive MIME type lookup
193 30 : const std::string& type = mime_types::extension_to_type(path.extension().string());
194 :
195 : // mime_types returns text/plain for unknown, but for form uploads
196 : // application/octet-stream is more appropriate
197 30 : if (type == mime_types::text_plain && !path.extension().empty()) {
198 2 : return mime_types::application_octect_stream;
199 : }
200 :
201 28 : return type;
202 : }
203 :
204 : } // namespace thinger::http
|