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
13QT_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
33class QFormDataPartBuilderPrivate
34{
35public:
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
46QFormDataPartBuilderPrivate::QFormDataPartBuilderPrivate(QAnyStringView name)
47 : m_name{name.toString()}
48{
49
50}
51
52
53static void escapeNameAndAppend(QByteArray &dst, QByteArrayView src)
54{
55 for (auto c : src) {
56 if (c == '"' || c == '\\')
57 dst += '\\';
58 dst += c;
59 }
60}
61
62static 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
144static void convertInto_impl(QByteArray &dst, QUtf8StringView in)
145{
146 dst.clear();
147 dst += QByteArrayView{in}; // it's ASCII, anyway
148}
149
150static void convertInto_impl(QByteArray &dst, QLatin1StringView in)
151{
152 dst.clear();
153 dst += QByteArrayView{in}; // it's ASCII, anyway
154}
155
156static void convertInto_impl(QByteArray &dst, QStringView in)
157{
158 dst.resize(size: in.size());
159 (void)QLatin1::convertFromUnicode(out: dst.data(), in);
160}
161
162static void convertInto(QByteArray &dst, QAnyStringView in)
163{
164 in.visit(v: [&dst](auto in) { convertInto_impl(dst, in); });
165}
166
167QFormDataPartBuilder 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
195QFormDataPartBuilder 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
227QFormDataPartBuilder 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
245QFormDataPartBuilder 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
263QHttpPart 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
375class QFormDataBuilderPrivate
376{
377public:
378 std::vector<QFormDataPartBuilderPrivate> parts;
379};
380
381QFormDataPartBuilderPrivate* QFormDataPartBuilder::d_func()
382{
383 return const_cast<QFormDataPartBuilderPrivate*>(std::as_const(t&: *this).d_func());
384}
385
386const 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
446QFormDataBuilder::QFormDataBuilder()
447 : d_ptr(new QFormDataBuilderPrivate())
448{
449
450}
451
452/*!
453 Destroys the QFormDataBuilder object.
454*/
455
456QFormDataBuilder::~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
486QFormDataPartBuilder 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
501std::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
513QT_END_NAMESPACE
514

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/network/access/qformdatabuilder.cpp