1 | // Copyright (C) 2024 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qformdatabuilder.h" |
5 | |
6 | #include <QtCore/private/qstringconverter_p.h> |
7 | #if QT_CONFIG(mimetype) |
8 | #include "QtCore/qmimedatabase.h" |
9 | #endif |
10 | |
11 | #include <vector> |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | /*! |
16 | \class QFormDataPartBuilder |
17 | \brief The QFormDataPartBuilder class is a convenience class to simplify |
18 | the construction of QHttpPart objects. |
19 | \since 6.8 |
20 | |
21 | \ingroup network |
22 | \ingroup shared |
23 | \inmodule QtNetwork |
24 | |
25 | The QFormDataPartBuilder class can be used to build a QHttpPart object with |
26 | the content disposition header set to be form-data by default. Then the |
27 | generated object can be used as part of a multipart message (which is |
28 | represented by the QHttpMultiPart class). |
29 | |
30 | \sa QHttpPart, QHttpMultiPart, QFormDataBuilder |
31 | */ |
32 | |
33 | class QFormDataPartBuilderPrivate |
34 | { |
35 | public: |
36 | explicit QFormDataPartBuilderPrivate(QAnyStringView name); |
37 | QHttpPart build(QFormDataBuilder::Options options); |
38 | |
39 | QString m_name; |
40 | QByteArray m_mimeType; |
41 | QString m_originalBodyName; |
42 | QHttpHeaders m_httpHeaders; |
43 | std::variant<QIODevice*, QByteArray> m_body; |
44 | }; |
45 | |
46 | QFormDataPartBuilderPrivate::QFormDataPartBuilderPrivate(QAnyStringView name) |
47 | : m_name{name.toString()} |
48 | { |
49 | |
50 | } |
51 | |
52 | |
53 | static void escapeNameAndAppend(QByteArray &dst, QByteArrayView src) |
54 | { |
55 | for (auto c : src) { |
56 | if (c == '"' || c == '\\') |
57 | dst += '\\'; |
58 | dst += c; |
59 | } |
60 | } |
61 | |
62 | static void escapeNameAndAppend(QByteArray &dst, QStringView src) |
63 | { |
64 | qsizetype last = 0; |
65 | |
66 | // equivalent to for (auto chunk : qTokenize(src, any_of("\\\""))), if there was such a thing |
67 | for (qsizetype i = 0, end = src.size(); i != end; ++i) { |
68 | const auto c = src[i]; |
69 | if (c == u'"' || c == u'\\') { |
70 | const auto chunk = src.sliced(pos: last, n: i - last); |
71 | dst += QUtf8::convertFromUnicode(in: chunk); // ### optimize |
72 | dst += '\\'; |
73 | last = i; |
74 | } |
75 | } |
76 | dst += QUtf8::convertFromUnicode(in: src.sliced(pos: last)); |
77 | } |
78 | |
79 | /*! |
80 | \fn QFormDataPartBuilder::QFormDataPartBuilder(QFormDataPartBuilder &&other) noexcept |
81 | |
82 | Move-constructs a QFormDataPartBuilder instance, making it point at the same |
83 | object that \a other was pointing to. |
84 | */ |
85 | |
86 | /*! |
87 | \fn QFormDataPartBuilder &QFormDataPartBuilder::operator=(QFormDataPartBuilder &&other) |
88 | |
89 | Move-assigns \a other to this QFormDataPartBuilder instance. |
90 | */ |
91 | |
92 | /*! |
93 | \fn QFormDataPartBuilder::QFormDataPartBuilder(const QFormDataPartBuilder &other) |
94 | |
95 | Constructs a copy of \a other. The object is valid for as long as the associated |
96 | QFormDataBuilder has not been destroyed. |
97 | |
98 | The data of the copy is shared (shallow copy): modifying one part will also change |
99 | the other. |
100 | |
101 | \code |
102 | QFormDataPartBuilder foo() |
103 | { |
104 | QFormDataBuilder builder; |
105 | auto qfdpb1 = builder.part("First"_L1); |
106 | auto qfdpb2 = qfdpb1; // this creates a shallow copy |
107 | |
108 | qfdpb2.setBodyDevice(&image, "cutecat.jpg"); // qfdpb1 is also modified |
109 | |
110 | return qfdbp2; // invalid, builder is destroyed at the end of the scope |
111 | } |
112 | \endcode |
113 | */ |
114 | |
115 | /*! |
116 | \fn QFormDataPartBuilder& QFormDataPartBuilder::operator=(const QFormDataPartBuilder &other) |
117 | |
118 | Assigns \a other to QFormDataPartBuilder and returns a reference to this |
119 | QFormDataPartBuilder. The object is valid for as long as the associated QFormDataBuilder |
120 | has not been destroyed. |
121 | |
122 | The data of the copy is shared (shallow copy): modifying one part will also change the other. |
123 | |
124 | \code |
125 | QFormDataPartBuilder foo() |
126 | { |
127 | QFormDataBuilder builder; |
128 | auto qfdpb1 = builder.part("First"_L1); |
129 | auto qfdpb2 = qfdpb1; // this creates a shallow copy |
130 | |
131 | qfdpb2.setBodyDevice(&image, "cutecat.jpg"); // qfdpb1 is also modified |
132 | |
133 | return qfdbp2; // invalid, builder is destroyed at the end of the scope |
134 | } |
135 | \endcode |
136 | */ |
137 | |
138 | /*! |
139 | \fn QFormDataPartBuilder::~QFormDataPartBuilder() |
140 | |
141 | Destroys the QFormDataPartBuilder object. |
142 | */ |
143 | |
144 | static void convertInto_impl(QByteArray &dst, QUtf8StringView in) |
145 | { |
146 | dst.clear(); |
147 | dst += QByteArrayView{in}; // it's ASCII, anyway |
148 | } |
149 | |
150 | static void convertInto_impl(QByteArray &dst, QLatin1StringView in) |
151 | { |
152 | dst.clear(); |
153 | dst += QByteArrayView{in}; // it's ASCII, anyway |
154 | } |
155 | |
156 | static void convertInto_impl(QByteArray &dst, QStringView in) |
157 | { |
158 | dst.resize(size: in.size()); |
159 | (void)QLatin1::convertFromUnicode(out: dst.data(), in); |
160 | } |
161 | |
162 | static void convertInto(QByteArray &dst, QAnyStringView in) |
163 | { |
164 | in.visit(v: [&dst](auto in) { convertInto_impl(dst, in); }); |
165 | } |
166 | |
167 | QFormDataPartBuilder QFormDataPartBuilder::setBodyHelper(const QByteArray &data, |
168 | QAnyStringView name, |
169 | QAnyStringView mimeType) |
170 | { |
171 | Q_D(QFormDataPartBuilder); |
172 | |
173 | d->m_originalBodyName = name.toString(); |
174 | convertInto(dst&: d->m_mimeType, in: mimeType); |
175 | d->m_body = data; |
176 | return *this; |
177 | } |
178 | |
179 | /*! |
180 | Sets \a data as the body of this MIME part and, if given, \a fileName as the |
181 | file name parameter in the content disposition header. |
182 | |
183 | If \a mimeType is not given (is empty), then QFormDataPartBuilder tries to |
184 | auto-detect the mime-type of \a data using QMimeDatabase. |
185 | |
186 | A subsequent call to setBodyDevice() discards the body and the device will |
187 | be used instead. |
188 | |
189 | For a large amount of data (e.g. an image), setBodyDevice() is preferred, |
190 | which will not copy the data internally. |
191 | |
192 | \sa setBodyDevice() |
193 | */ |
194 | |
195 | QFormDataPartBuilder QFormDataPartBuilder::setBody(QByteArrayView data, |
196 | QAnyStringView fileName, |
197 | QAnyStringView mimeType) |
198 | { |
199 | return setBody(data: data.toByteArray(), fileName, mimeType); |
200 | } |
201 | |
202 | /*! |
203 | Sets \a body as the body device of this part and \a fileName as the file |
204 | name parameter in the content disposition header. |
205 | |
206 | If \a mimeType is not given (is empty), then QFormDataPartBuilder tries to |
207 | auto-detect the mime-type of \a body using QMimeDatabase. |
208 | |
209 | A subsequent call to setBody() discards the body device and the data set by |
210 | setBody() will be used instead. |
211 | |
212 | For large amounts of data this method should be preferred over setBody(), |
213 | because the content is not copied when using this method, but read |
214 | directly from the device. |
215 | |
216 | \a body must be open and readable. QFormDataPartBuilder does not take |
217 | ownership of \a body, i.e. the device must be closed and destroyed if |
218 | necessary. |
219 | |
220 | \note If \a body is sequential (e.g. sockets, but not files), |
221 | QNetworkAccessManager::post() should be called after \a body has emitted |
222 | finished(). |
223 | |
224 | \sa setBody(), QHttpPart::setBodyDevice() |
225 | */ |
226 | |
227 | QFormDataPartBuilder QFormDataPartBuilder::setBodyDevice(QIODevice *body, QAnyStringView fileName, |
228 | QAnyStringView mimeType) |
229 | { |
230 | Q_D(QFormDataPartBuilder); |
231 | |
232 | d->m_originalBodyName = fileName.toString(); |
233 | convertInto(dst&: d->m_mimeType, in: mimeType); |
234 | d->m_body = body; |
235 | return *this; |
236 | } |
237 | |
238 | /*! |
239 | Sets the headers specified in \a headers. |
240 | |
241 | \note The "content-type" and "content-disposition" headers, if any are |
242 | specified in \a headers, will be overwritten by the class. |
243 | */ |
244 | |
245 | QFormDataPartBuilder QFormDataPartBuilder::setHeaders(const QHttpHeaders &headers) |
246 | { |
247 | Q_D(QFormDataPartBuilder); |
248 | |
249 | d->m_httpHeaders = headers; |
250 | return *this; |
251 | } |
252 | |
253 | /*! |
254 | \internal |
255 | |
256 | Generates a QHttpPart and sets the content disposition header as form-data. |
257 | |
258 | When this function called, it uses the MIME database to deduce the type the |
259 | body based on its name and then sets the deduced type as the content type |
260 | header. |
261 | */ |
262 | |
263 | QHttpPart QFormDataPartBuilderPrivate::build(QFormDataBuilder::Options options) |
264 | { |
265 | QHttpPart httpPart; |
266 | |
267 | using Opt = QFormDataBuilder::Option; |
268 | |
269 | QByteArray headerValue; |
270 | |
271 | headerValue += "form-data; name=\""; |
272 | escapeNameAndAppend(dst&: headerValue, src: m_name); |
273 | headerValue += "\""; |
274 | |
275 | if (!m_originalBodyName.isNull()) { |
276 | |
277 | enum class Encoding { ASCII, Latin1, Utf8 } encoding = Encoding::ASCII; |
278 | for (QChar c : std::as_const(t&: m_originalBodyName)) { |
279 | if (c > u'\xff') { |
280 | encoding = Encoding::Utf8; |
281 | break; |
282 | } else if (c > u'\x7f') { |
283 | encoding = Encoding::Latin1; |
284 | } |
285 | } |
286 | QByteArray enc; |
287 | if ((options & Opt::PreferLatin1EncodedFilename) && encoding != Encoding::Utf8) |
288 | enc = m_originalBodyName.toLatin1(); |
289 | else |
290 | enc = m_originalBodyName.toUtf8(); |
291 | |
292 | headerValue += "; filename=\""; |
293 | if (options & Opt::UseRfc7578PercentEncodedFilename) |
294 | headerValue += enc.toPercentEncoding(); |
295 | else |
296 | escapeNameAndAppend(dst&: headerValue, src: enc); |
297 | headerValue += "\""; |
298 | if (encoding != Encoding::ASCII && !(options & Opt::OmitRfc8187EncodedFilename)) { |
299 | // For 'filename*' production see |
300 | // https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1 |
301 | // For providing both filename and filename* parameters see |
302 | // https://datatracker.ietf.org/doc/html/rfc6266#section-4.3 and |
303 | // https://datatracker.ietf.org/doc/html/rfc8187#section-4.2 |
304 | if ((options & Opt::PreferLatin1EncodedFilename) && encoding == Encoding::Latin1) |
305 | headerValue += "; filename*=ISO-8859-1''"; |
306 | else |
307 | headerValue += "; filename*=UTF-8''"; |
308 | headerValue += enc.toPercentEncoding(); |
309 | } |
310 | } |
311 | |
312 | #if QT_CONFIG(mimetype) |
313 | if (m_mimeType.isEmpty()) { |
314 | // auto-detect |
315 | QMimeDatabase db; |
316 | convertInto(dst&: m_mimeType, in: std::visit(visitor: [&](auto &arg) { |
317 | return db.mimeTypeForFileNameAndData(m_originalBodyName, arg); |
318 | }, variants&: m_body).name()); |
319 | } |
320 | #endif |
321 | |
322 | for (qsizetype i = 0; i < m_httpHeaders.size(); i++) { |
323 | httpPart.setRawHeader(headerName: QByteArrayView(m_httpHeaders.nameAt(i)).toByteArray(), |
324 | headerValue: m_httpHeaders.valueAt(i).toByteArray()); |
325 | } |
326 | |
327 | if (!m_mimeType.isEmpty()) |
328 | httpPart.setHeader(header: QNetworkRequest::ContentTypeHeader, value: m_mimeType); |
329 | |
330 | httpPart.setHeader(header: QNetworkRequest::ContentDispositionHeader, value: std::move(headerValue)); |
331 | |
332 | if (auto d = std::get_if<QIODevice*>(ptr: &m_body)) |
333 | httpPart.setBodyDevice(*d); |
334 | else if (auto b = std::get_if<QByteArray>(ptr: &m_body)) |
335 | httpPart.setBody(*b); |
336 | else |
337 | Q_UNREACHABLE(); |
338 | |
339 | return httpPart; |
340 | } |
341 | |
342 | /*! |
343 | \class QFormDataBuilder |
344 | \brief The QFormDataBuilder class is a convenience class to simplify |
345 | the construction of QHttpMultiPart objects. |
346 | \since 6.8 |
347 | |
348 | \ingroup network |
349 | \ingroup shared |
350 | \inmodule QtNetwork |
351 | |
352 | The QFormDataBuilder class can be used to build a QHttpMultiPart object |
353 | with the content type set to be FormDataType by default. |
354 | |
355 | The snippet below demonstrates how to build a multipart message with |
356 | QFormDataBuilder: |
357 | |
358 | \code |
359 | QFormDataBuilder builder; |
360 | QFile image(u"../../pic.png"_s); image.open(QFile::ReadOnly); |
361 | QFile mask(u"../../mask.png"_s); mask.open(QFile::ReadOnly); |
362 | |
363 | builder.part("image"_L1).setBodyDevice(&image, "the actual image"); |
364 | builder.part("mask"_L1).setBodyDevice(&mask, "the mask image"); |
365 | builder.part("prompt"_L1).setBody("Lobster wearing a beret"); |
366 | builder.part("n"_L1).setBody("2"); |
367 | builder.part("size"_L1).setBody("512x512"); |
368 | |
369 | std::unique_ptr<QHttpMultiPart> mp = builder.buildMultiPart(); |
370 | \endcode |
371 | |
372 | \sa QHttpPart, QHttpMultiPart, QFormDataPartBuilder |
373 | */ |
374 | |
375 | class QFormDataBuilderPrivate |
376 | { |
377 | public: |
378 | std::vector<QFormDataPartBuilderPrivate> parts; |
379 | }; |
380 | |
381 | QFormDataPartBuilderPrivate* QFormDataPartBuilder::d_func() |
382 | { |
383 | return const_cast<QFormDataPartBuilderPrivate*>(std::as_const(t&: *this).d_func()); |
384 | } |
385 | |
386 | const QFormDataPartBuilderPrivate* QFormDataPartBuilder::d_func() const |
387 | { |
388 | Q_ASSERT(m_index < d->parts.size()); |
389 | return &d->parts[m_index]; |
390 | } |
391 | |
392 | /*! |
393 | \enum QFormDataBuilder::Option |
394 | |
395 | Options controlling buildMultiPart(). |
396 | |
397 | Several current RFCs disagree on how, exactly, to format |
398 | \c{multipart/form-data}. Instead of hard-coding any one RFC, these options |
399 | give you control over which RFC to follow. |
400 | |
401 | \value Default The default values, designed to maximize interoperability in |
402 | general. All options named below are off. |
403 | |
404 | \value OmitRfc8187EncodedFilename |
405 | When a body-part's file-name contains non-US-ASCII characters, |
406 | \l{https://datatracker.ietf.org/doc/html/rfc6266#section-4.3} |
407 | {RFC 6266 Section 4.3} suggests to use |
408 | \l{https://datatracker.ietf.org/doc/html/rfc8187}{RFC 8187}-style |
409 | encoding (\c{filename*=utf-8''...}). The more recent |
410 | \l{https://datatracker.ietf.org/doc/html/rfc7578#section-4.2} |
411 | {RFC 7578 Section 4.2}, however, bans the use of that mechanism. |
412 | Both RFCs are current as of this writing, so this option allows you |
413 | to choose which one to follow. The default is to include the |
414 | RFC 8187-encoded \c{filename*} alongside the unencoded \c{filename}, |
415 | as suggested by RFC 6266. |
416 | |
417 | \value UseRfc7578PercentEncodedFilename |
418 | When a body-part's file-name contains non-US-ASCII characters, |
419 | \l{https://datatracker.ietf.org/doc/html/rfc7578#section-4.2} |
420 | {RFC 7578 Section 4.2} suggests to use percent-encoding of the octets |
421 | of the UTF-8-encoded file-name. It goes on to note that many |
422 | implementations, however, do \e{not} percent-encode the UTF-8-encoded |
423 | file-name, but just emit "raw" UTF-8 (with \c{"} and \c{\} escaped |
424 | using \c{\}). This is the default of QFormDataBuilder, too. |
425 | |
426 | \value PreferLatin1EncodedFilename |
427 | \l{https://datatracker.ietf.org/doc/html/rfc5987#section-3.2} |
428 | {RFC 5987 Section 3.2} required recipients to support ISO-8859-1 |
429 | ("Latin-1") encoding. When a body-part's file-name contains |
430 | non-US-ASCII characters that, however, fit into Latin-1, this option |
431 | prefers to use ISO-8859-1 encoding over UTF-8. The more recent |
432 | \{https://datatracker.ietf.org/doc/html/rfc8187#appendix-A}{RFC 8187} |
433 | no longer requires ISO-8859-1 support, so the default is to send all |
434 | non-US-ASCII file-names in UTF-8 encoding instead. |
435 | |
436 | \value StrictRfc7578 |
437 | This option combines other options to select strict |
438 | \l{https://datatracker.ietf.org/doc/html/rfc7578}{RFC 7578} |
439 | compliance. |
440 | */ |
441 | |
442 | /*! |
443 | Constructs an empty QFormDataBuilder object. |
444 | */ |
445 | |
446 | QFormDataBuilder::QFormDataBuilder() |
447 | : d_ptr(new QFormDataBuilderPrivate()) |
448 | { |
449 | |
450 | } |
451 | |
452 | /*! |
453 | Destroys the QFormDataBuilder object. |
454 | */ |
455 | |
456 | QFormDataBuilder::~QFormDataBuilder() |
457 | { |
458 | delete d_ptr; |
459 | } |
460 | |
461 | /*! |
462 | \fn QFormDataBuilder::QFormDataBuilder(QFormDataBuilder &&other) noexcept |
463 | |
464 | Move-constructs a QFormDataBuilder instance, making it point at the same |
465 | object that \a other was pointing to. |
466 | */ |
467 | |
468 | /*! |
469 | \fn QFormDataBuilder &QFormDataBuilder::operator=(QFormDataBuilder &&other) noexcept |
470 | |
471 | Move-assigns \a other to this QFormDataBuilder instance. |
472 | */ |
473 | |
474 | /*! |
475 | Returns a newly-constructed QFormDataPartBuilder object using \a name as the |
476 | form-data's \c name parameter. The object is valid for as long as the |
477 | associated QFormDataBuilder has not been destroyed. |
478 | |
479 | Limiting \a name characters to US-ASCII is |
480 | \l {https://datatracker.ietf.org/doc/html/rfc7578#section-5.1.1}{strongly recommended} |
481 | for interoperability reasons. |
482 | |
483 | \sa QFormDataPartBuilder, QHttpPart |
484 | */ |
485 | |
486 | QFormDataPartBuilder QFormDataBuilder::part(QAnyStringView name) |
487 | { |
488 | Q_D(QFormDataBuilder); |
489 | |
490 | d->parts.emplace_back(args&: name); |
491 | return QFormDataPartBuilder(d, d->parts.size() - 1); |
492 | } |
493 | |
494 | /*! |
495 | Constructs and returns a pointer to a QHttpMultipart object constructed |
496 | according to \a options. |
497 | |
498 | \sa QHttpMultiPart |
499 | */ |
500 | |
501 | std::unique_ptr<QHttpMultiPart> QFormDataBuilder::buildMultiPart(Options options) |
502 | { |
503 | Q_D(QFormDataBuilder); |
504 | |
505 | auto multiPart = std::make_unique<QHttpMultiPart>(args: QHttpMultiPart::FormDataType); |
506 | |
507 | for (auto &part : d->parts) |
508 | multiPart->append(httpPart: part.build(options)); |
509 | |
510 | return multiPart; |
511 | } |
512 | |
513 | QT_END_NAMESPACE |
514 |
Definitions
Learn Advanced QML with KDAB
Find out more