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