1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtNetwork module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qnetworkreplyimpl_p.h" |
41 | #include "qnetworkaccessbackend_p.h" |
42 | #include "qnetworkcookie.h" |
43 | #include "qnetworkcookiejar.h" |
44 | #include "qabstractnetworkcache.h" |
45 | #include "QtCore/qcoreapplication.h" |
46 | #include "QtCore/qdatetime.h" |
47 | #include "QtNetwork/qsslconfiguration.h" |
48 | #include "QtNetwork/qnetworksession.h" // ### Qt6: Remove include |
49 | #include "qnetworkaccessmanager_p.h" |
50 | |
51 | #include <QtCore/QCoreApplication> |
52 | |
53 | QT_BEGIN_NAMESPACE |
54 | |
55 | inline QNetworkReplyImplPrivate::QNetworkReplyImplPrivate() |
56 | : backend(nullptr), outgoingData(nullptr), |
57 | copyDevice(nullptr), |
58 | cacheEnabled(false), cacheSaveDevice(nullptr), |
59 | notificationHandlingPaused(false), |
60 | bytesDownloaded(0), lastBytesDownloaded(-1), bytesUploaded(-1), preMigrationDownloaded(-1), |
61 | httpStatusCode(0), |
62 | state(Idle) |
63 | , downloadBufferReadPosition(0) |
64 | , downloadBufferCurrentSize(0) |
65 | , downloadBufferMaximumSize(0) |
66 | , downloadBuffer(nullptr) |
67 | { |
68 | if (request.attribute(code: QNetworkRequest::EmitAllUploadProgressSignalsAttribute).toBool() == true) |
69 | emitAllUploadProgressSignals = true; |
70 | } |
71 | |
72 | void QNetworkReplyImplPrivate::_q_startOperation() |
73 | { |
74 | // ensure this function is only being called once |
75 | if (state == Working || state == Finished) { |
76 | qDebug() << "QNetworkReplyImpl::_q_startOperation was called more than once" << url; |
77 | return; |
78 | } |
79 | state = Working; |
80 | |
81 | // note: if that method is called directly, it cannot happen that the backend is 0, |
82 | // because we just checked via a qobject_cast that we got a http backend (see |
83 | // QNetworkReplyImplPrivate::setup()) |
84 | if (!backend) { |
85 | error(code: QNetworkReplyImpl::ProtocolUnknownError, |
86 | errorString: QCoreApplication::translate(context: "QNetworkReply" , key: "Protocol \"%1\" is unknown" ).arg(a: url.scheme())); // not really true!; |
87 | finished(); |
88 | return; |
89 | } |
90 | |
91 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
92 | Q_Q(QNetworkReplyImpl); |
93 | // Do not start background requests if they are not allowed by session policy |
94 | QSharedPointer<QNetworkSession> session(manager->d_func()->getNetworkSession()); |
95 | QVariant isBackground = backend->request().attribute(code: QNetworkRequest::BackgroundRequestAttribute, defaultValue: QVariant::fromValue(value: false)); |
96 | if (isBackground.toBool() && session && session->usagePolicies().testFlag(flag: QNetworkSession::NoBackgroundTrafficPolicy)) { |
97 | error(code: QNetworkReply::BackgroundRequestNotAllowedError, |
98 | errorString: QCoreApplication::translate(context: "QNetworkReply" , key: "Background request not allowed." )); |
99 | finished(); |
100 | return; |
101 | } |
102 | #endif |
103 | |
104 | if (!backend->start()) { |
105 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
106 | // backend failed to start because the session state is not Connected. |
107 | // QNetworkAccessManager will call _q_startOperation again for us when the session |
108 | // state changes. |
109 | state = WaitingForSession; |
110 | |
111 | if (session) { |
112 | QObject::connect(sender: session.data(), SIGNAL(error(QNetworkSession::SessionError)), |
113 | receiver: q, SLOT(_q_networkSessionFailed())); |
114 | |
115 | if (!session->isOpen()) { |
116 | session->setSessionProperty(QStringLiteral("ConnectInBackground" ), value: isBackground); |
117 | session->open(); |
118 | } |
119 | } else { |
120 | qWarning(msg: "Backend is waiting for QNetworkSession to connect, but there is none!" ); |
121 | state = Working; |
122 | error(code: QNetworkReplyImpl::NetworkSessionFailedError, |
123 | errorString: QCoreApplication::translate(context: "QNetworkReply" , key: "Network session error." )); |
124 | finished(); |
125 | } |
126 | #else |
127 | qWarning("Backend start failed" ); |
128 | state = Working; |
129 | error(QNetworkReplyImpl::UnknownNetworkError, |
130 | QCoreApplication::translate("QNetworkReply" , "backend start error." )); |
131 | finished(); |
132 | #endif |
133 | return; |
134 | } else { |
135 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
136 | if (session) { |
137 | QObject::connect(sender: session.data(), SIGNAL(stateChanged(QNetworkSession::State)), |
138 | receiver: q, SLOT(_q_networkSessionStateChanged(QNetworkSession::State)), Qt::QueuedConnection); |
139 | } |
140 | #endif |
141 | } |
142 | |
143 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
144 | if (session) { |
145 | //get notification of policy changes. |
146 | QObject::connect(sender: session.data(), SIGNAL(usagePoliciesChanged(QNetworkSession::UsagePolicies)), |
147 | receiver: q, SLOT(_q_networkSessionUsagePoliciesChanged(QNetworkSession::UsagePolicies))); |
148 | } |
149 | #endif |
150 | |
151 | // Prepare timer for progress notifications |
152 | downloadProgressSignalChoke.start(); |
153 | uploadProgressSignalChoke.invalidate(); |
154 | |
155 | if (backend && backend->isSynchronous()) { |
156 | state = Finished; |
157 | q_func()->setFinished(true); |
158 | } else { |
159 | if (state != Finished) { |
160 | if (operation == QNetworkAccessManager::GetOperation) |
161 | pendingNotifications.push_back(x: NotifyDownstreamReadyWrite); |
162 | |
163 | handleNotifications(); |
164 | } |
165 | } |
166 | } |
167 | |
168 | void QNetworkReplyImplPrivate::_q_copyReadyRead() |
169 | { |
170 | Q_Q(QNetworkReplyImpl); |
171 | if (state != Working) |
172 | return; |
173 | if (!copyDevice || !q->isOpen()) |
174 | return; |
175 | |
176 | // FIXME Optimize to use download buffer if it is a QBuffer. |
177 | // Needs to be done where sendCacheContents() (?) of HTTP is emitting |
178 | // metaDataChanged ? |
179 | |
180 | forever { |
181 | qint64 bytesToRead = nextDownstreamBlockSize(); |
182 | if (bytesToRead == 0) |
183 | // we'll be called again, eventually |
184 | break; |
185 | |
186 | bytesToRead = qBound<qint64>(min: 1, val: bytesToRead, max: copyDevice->bytesAvailable()); |
187 | qint64 bytesActuallyRead = copyDevice->read(data: buffer.reserve(bytes: bytesToRead), maxlen: bytesToRead); |
188 | if (bytesActuallyRead == -1) { |
189 | buffer.chop(bytes: bytesToRead); |
190 | backendNotify(notification: NotifyCopyFinished); |
191 | break; |
192 | } |
193 | buffer.chop(bytes: bytesToRead - bytesActuallyRead); |
194 | |
195 | if (!copyDevice->isSequential() && copyDevice->atEnd()) { |
196 | backendNotify(notification: NotifyCopyFinished); |
197 | bytesDownloaded += bytesActuallyRead; |
198 | break; |
199 | } |
200 | |
201 | bytesDownloaded += bytesActuallyRead; |
202 | } |
203 | |
204 | if (bytesDownloaded == lastBytesDownloaded) { |
205 | // we didn't read anything |
206 | return; |
207 | } |
208 | |
209 | lastBytesDownloaded = bytesDownloaded; |
210 | QVariant totalSize = cookedHeaders.value(key: QNetworkRequest::ContentLengthHeader); |
211 | if (preMigrationDownloaded != Q_INT64_C(-1)) |
212 | totalSize = totalSize.toLongLong() + preMigrationDownloaded; |
213 | pauseNotificationHandling(); |
214 | // emit readyRead before downloadProgress incase this will cause events to be |
215 | // processed and we get into a recursive call (as in QProgressDialog). |
216 | emit q->readyRead(); |
217 | if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
218 | downloadProgressSignalChoke.restart(); |
219 | emit q->downloadProgress(bytesReceived: bytesDownloaded, |
220 | bytesTotal: totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); |
221 | } |
222 | resumeNotificationHandling(); |
223 | } |
224 | |
225 | void QNetworkReplyImplPrivate::_q_copyReadChannelFinished() |
226 | { |
227 | _q_copyReadyRead(); |
228 | } |
229 | |
230 | void QNetworkReplyImplPrivate::_q_bufferOutgoingDataFinished() |
231 | { |
232 | Q_Q(QNetworkReplyImpl); |
233 | |
234 | // make sure this is only called once, ever. |
235 | //_q_bufferOutgoingData may call it or the readChannelFinished emission |
236 | if (state != Buffering) |
237 | return; |
238 | |
239 | // disconnect signals |
240 | QObject::disconnect(sender: outgoingData, SIGNAL(readyRead()), receiver: q, SLOT(_q_bufferOutgoingData())); |
241 | QObject::disconnect(sender: outgoingData, SIGNAL(readChannelFinished()), receiver: q, SLOT(_q_bufferOutgoingDataFinished())); |
242 | |
243 | // finally, start the request |
244 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
245 | } |
246 | |
247 | void QNetworkReplyImplPrivate::_q_bufferOutgoingData() |
248 | { |
249 | Q_Q(QNetworkReplyImpl); |
250 | |
251 | if (!outgoingDataBuffer) { |
252 | // first call, create our buffer |
253 | outgoingDataBuffer = QSharedPointer<QRingBuffer>::create(); |
254 | |
255 | QObject::connect(sender: outgoingData, SIGNAL(readyRead()), receiver: q, SLOT(_q_bufferOutgoingData())); |
256 | QObject::connect(sender: outgoingData, SIGNAL(readChannelFinished()), receiver: q, SLOT(_q_bufferOutgoingDataFinished())); |
257 | } |
258 | |
259 | qint64 bytesBuffered = 0; |
260 | qint64 bytesToBuffer = 0; |
261 | |
262 | // read data into our buffer |
263 | forever { |
264 | bytesToBuffer = outgoingData->bytesAvailable(); |
265 | // unknown? just try 2 kB, this also ensures we always try to read the EOF |
266 | if (bytesToBuffer <= 0) |
267 | bytesToBuffer = 2*1024; |
268 | |
269 | char *dst = outgoingDataBuffer->reserve(bytes: bytesToBuffer); |
270 | bytesBuffered = outgoingData->read(data: dst, maxlen: bytesToBuffer); |
271 | |
272 | if (bytesBuffered == -1) { |
273 | // EOF has been reached. |
274 | outgoingDataBuffer->chop(bytes: bytesToBuffer); |
275 | |
276 | _q_bufferOutgoingDataFinished(); |
277 | break; |
278 | } else if (bytesBuffered == 0) { |
279 | // nothing read right now, just wait until we get called again |
280 | outgoingDataBuffer->chop(bytes: bytesToBuffer); |
281 | |
282 | break; |
283 | } else { |
284 | // don't break, try to read() again |
285 | outgoingDataBuffer->chop(bytes: bytesToBuffer - bytesBuffered); |
286 | } |
287 | } |
288 | } |
289 | |
290 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
291 | void QNetworkReplyImplPrivate::_q_networkSessionConnected() |
292 | { |
293 | Q_Q(QNetworkReplyImpl); |
294 | |
295 | if (manager.isNull()) |
296 | return; |
297 | |
298 | QSharedPointer<QNetworkSession> session = manager->d_func()->getNetworkSession(); |
299 | if (!session) |
300 | return; |
301 | |
302 | if (session->state() != QNetworkSession::Connected) |
303 | return; |
304 | |
305 | switch (state) { |
306 | case QNetworkReplyPrivate::Buffering: |
307 | case QNetworkReplyPrivate::Working: |
308 | case QNetworkReplyPrivate::Reconnecting: |
309 | // Migrate existing downloads to new network connection. |
310 | migrateBackend(); |
311 | break; |
312 | case QNetworkReplyPrivate::WaitingForSession: |
313 | // Start waiting requests. |
314 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
315 | break; |
316 | default: |
317 | ; |
318 | } |
319 | } |
320 | |
321 | void QNetworkReplyImplPrivate::_q_networkSessionStateChanged(QNetworkSession::State sessionState) |
322 | { |
323 | if (sessionState == QNetworkSession::Disconnected |
324 | && state != Idle && state != Reconnecting) { |
325 | error(code: QNetworkReplyImpl::NetworkSessionFailedError, |
326 | errorString: QCoreApplication::translate(context: "QNetworkReply" , key: "Network session error." )); |
327 | finished(); |
328 | } |
329 | } |
330 | |
331 | void QNetworkReplyImplPrivate::_q_networkSessionFailed() |
332 | { |
333 | // Abort waiting and working replies. |
334 | if (state == WaitingForSession || state == Working) { |
335 | state = Working; |
336 | QSharedPointer<QNetworkSession> session(manager->d_func()->getNetworkSession()); |
337 | QString errorStr; |
338 | if (session) |
339 | errorStr = session->errorString(); |
340 | else |
341 | errorStr = QCoreApplication::translate(context: "QNetworkReply" , key: "Network session error." ); |
342 | error(code: QNetworkReplyImpl::NetworkSessionFailedError, errorString: errorStr); |
343 | finished(); |
344 | } |
345 | } |
346 | |
347 | void QNetworkReplyImplPrivate::_q_networkSessionUsagePoliciesChanged(QNetworkSession::UsagePolicies newPolicies) |
348 | { |
349 | if (backend->request().attribute(code: QNetworkRequest::BackgroundRequestAttribute).toBool()) { |
350 | if (newPolicies & QNetworkSession::NoBackgroundTrafficPolicy) { |
351 | // Abort waiting and working replies. |
352 | if (state == WaitingForSession || state == Working) { |
353 | state = Working; |
354 | error(code: QNetworkReply::BackgroundRequestNotAllowedError, |
355 | errorString: QCoreApplication::translate(context: "QNetworkReply" , key: "Background request not allowed." )); |
356 | finished(); |
357 | } |
358 | // ### if backend->canResume(), then we could resume automatically, however no backend supports resuming |
359 | } |
360 | } |
361 | } |
362 | #endif |
363 | |
364 | void QNetworkReplyImplPrivate::setup(QNetworkAccessManager::Operation op, const QNetworkRequest &req, |
365 | QIODevice *data) |
366 | { |
367 | Q_Q(QNetworkReplyImpl); |
368 | |
369 | outgoingData = data; |
370 | request = req; |
371 | originalRequest = req; |
372 | url = request.url(); |
373 | operation = op; |
374 | |
375 | q->QIODevice::open(mode: QIODevice::ReadOnly); |
376 | // Internal code that does a HTTP reply for the synchronous Ajax |
377 | // in Qt WebKit. |
378 | QVariant synchronousHttpAttribute = req.attribute( |
379 | code: static_cast<QNetworkRequest::Attribute>(QNetworkRequest::SynchronousRequestAttribute)); |
380 | // The synchronous HTTP is a corner case, we will put all upload data in one big QByteArray in the outgoingDataBuffer. |
381 | // Yes, this is not the most efficient thing to do, but on the other hand synchronous XHR needs to die anyway. |
382 | if (synchronousHttpAttribute.toBool() && outgoingData) { |
383 | outgoingDataBuffer = QSharedPointer<QRingBuffer>::create(); |
384 | qint64 previousDataSize = 0; |
385 | do { |
386 | previousDataSize = outgoingDataBuffer->size(); |
387 | outgoingDataBuffer->append(qba: outgoingData->readAll()); |
388 | } while (outgoingDataBuffer->size() != previousDataSize); |
389 | } |
390 | |
391 | if (backend) |
392 | backend->setSynchronous(synchronousHttpAttribute.toBool()); |
393 | |
394 | |
395 | if (outgoingData && backend && !backend->isSynchronous()) { |
396 | // there is data to be uploaded, e.g. HTTP POST. |
397 | |
398 | if (!backend->needsResetableUploadData() || !outgoingData->isSequential()) { |
399 | // backend does not need upload buffering or |
400 | // fixed size non-sequential |
401 | // just start the operation |
402 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
403 | } else { |
404 | bool bufferingDisallowed = |
405 | req.attribute(code: QNetworkRequest::DoNotBufferUploadDataAttribute, |
406 | defaultValue: false).toBool(); |
407 | |
408 | if (bufferingDisallowed) { |
409 | // if a valid content-length header for the request was supplied, we can disable buffering |
410 | // if not, we will buffer anyway |
411 | if (req.header(header: QNetworkRequest::ContentLengthHeader).isValid()) { |
412 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
413 | } else { |
414 | state = Buffering; |
415 | QMetaObject::invokeMethod(obj: q, member: "_q_bufferOutgoingData" , type: Qt::QueuedConnection); |
416 | } |
417 | } else { |
418 | // _q_startOperation will be called when the buffering has finished. |
419 | state = Buffering; |
420 | QMetaObject::invokeMethod(obj: q, member: "_q_bufferOutgoingData" , type: Qt::QueuedConnection); |
421 | } |
422 | } |
423 | } else { |
424 | // for HTTP, we want to send out the request as fast as possible to the network, without |
425 | // invoking methods in a QueuedConnection |
426 | if (backend && backend->isSynchronous()) |
427 | _q_startOperation(); |
428 | else |
429 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
430 | } |
431 | } |
432 | |
433 | void QNetworkReplyImplPrivate::backendNotify(InternalNotifications notification) |
434 | { |
435 | Q_Q(QNetworkReplyImpl); |
436 | const auto it = std::find(first: pendingNotifications.cbegin(), last: pendingNotifications.cend(), val: notification); |
437 | if (it == pendingNotifications.cend()) |
438 | pendingNotifications.push_back(x: notification); |
439 | |
440 | if (pendingNotifications.size() == 1) |
441 | QCoreApplication::postEvent(receiver: q, event: new QEvent(QEvent::NetworkReplyUpdated)); |
442 | } |
443 | |
444 | void QNetworkReplyImplPrivate::handleNotifications() |
445 | { |
446 | if (notificationHandlingPaused) |
447 | return; |
448 | |
449 | for (InternalNotifications notification : qExchange(t&: pendingNotifications, newValue: {})) { |
450 | if (state != Working) |
451 | return; |
452 | switch (notification) { |
453 | case NotifyDownstreamReadyWrite: |
454 | if (copyDevice) |
455 | _q_copyReadyRead(); |
456 | else |
457 | backend->downstreamReadyWrite(); |
458 | break; |
459 | |
460 | case NotifyCloseDownstreamChannel: |
461 | backend->closeDownstreamChannel(); |
462 | break; |
463 | |
464 | case NotifyCopyFinished: { |
465 | QIODevice *dev = qExchange(t&: copyDevice, newValue: nullptr); |
466 | backend->copyFinished(dev); |
467 | break; |
468 | } |
469 | } |
470 | } |
471 | } |
472 | |
473 | // Do not handle the notifications while we are emitting downloadProgress |
474 | // or readyRead |
475 | void QNetworkReplyImplPrivate::pauseNotificationHandling() |
476 | { |
477 | notificationHandlingPaused = true; |
478 | } |
479 | |
480 | // Resume notification handling |
481 | void QNetworkReplyImplPrivate::resumeNotificationHandling() |
482 | { |
483 | Q_Q(QNetworkReplyImpl); |
484 | notificationHandlingPaused = false; |
485 | if (pendingNotifications.size() >= 1) |
486 | QCoreApplication::postEvent(receiver: q, event: new QEvent(QEvent::NetworkReplyUpdated)); |
487 | } |
488 | |
489 | QAbstractNetworkCache *QNetworkReplyImplPrivate::networkCache() const |
490 | { |
491 | if (!backend) |
492 | return nullptr; |
493 | return backend->networkCache(); |
494 | } |
495 | |
496 | void QNetworkReplyImplPrivate::createCache() |
497 | { |
498 | // check if we can save and if we're allowed to |
499 | if (!networkCache() |
500 | || !request.attribute(code: QNetworkRequest::CacheSaveControlAttribute, defaultValue: true).toBool()) |
501 | return; |
502 | cacheEnabled = true; |
503 | } |
504 | |
505 | bool QNetworkReplyImplPrivate::isCachingEnabled() const |
506 | { |
507 | return (cacheEnabled && networkCache() != nullptr); |
508 | } |
509 | |
510 | void QNetworkReplyImplPrivate::setCachingEnabled(bool enable) |
511 | { |
512 | if (!enable && !cacheEnabled) |
513 | return; // nothing to do |
514 | if (enable && cacheEnabled) |
515 | return; // nothing to do either! |
516 | |
517 | if (enable) { |
518 | if (Q_UNLIKELY(bytesDownloaded)) { |
519 | // refuse to enable in this case |
520 | qCritical(msg: "QNetworkReplyImpl: backend error: caching was enabled after some bytes had been written" ); |
521 | return; |
522 | } |
523 | |
524 | createCache(); |
525 | } else { |
526 | // someone told us to turn on, then back off? |
527 | // ok... but you should make up your mind |
528 | qDebug(msg: "QNetworkReplyImpl: setCachingEnabled(true) called after setCachingEnabled(false) -- " |
529 | "backend %s probably needs to be fixed" , |
530 | backend->metaObject()->className()); |
531 | networkCache()->remove(url); |
532 | cacheSaveDevice = nullptr; |
533 | cacheEnabled = false; |
534 | } |
535 | } |
536 | |
537 | void QNetworkReplyImplPrivate::completeCacheSave() |
538 | { |
539 | if (cacheEnabled && errorCode != QNetworkReplyImpl::NoError) { |
540 | networkCache()->remove(url); |
541 | } else if (cacheEnabled && cacheSaveDevice) { |
542 | networkCache()->insert(device: cacheSaveDevice); |
543 | } |
544 | cacheSaveDevice = nullptr; |
545 | cacheEnabled = false; |
546 | } |
547 | |
548 | void QNetworkReplyImplPrivate::emitUploadProgress(qint64 bytesSent, qint64 bytesTotal) |
549 | { |
550 | Q_Q(QNetworkReplyImpl); |
551 | bytesUploaded = bytesSent; |
552 | |
553 | if (!emitAllUploadProgressSignals) { |
554 | //choke signal emissions, except the first and last signals which are unconditional |
555 | if (uploadProgressSignalChoke.isValid()) { |
556 | if (bytesSent != bytesTotal && uploadProgressSignalChoke.elapsed() < progressSignalInterval) { |
557 | return; |
558 | } |
559 | uploadProgressSignalChoke.restart(); |
560 | } else { |
561 | uploadProgressSignalChoke.start(); |
562 | } |
563 | } |
564 | |
565 | pauseNotificationHandling(); |
566 | emit q->uploadProgress(bytesSent, bytesTotal); |
567 | resumeNotificationHandling(); |
568 | } |
569 | |
570 | |
571 | qint64 QNetworkReplyImplPrivate::nextDownstreamBlockSize() const |
572 | { |
573 | enum { DesiredBufferSize = 32 * 1024 }; |
574 | if (readBufferMaxSize == 0) |
575 | return DesiredBufferSize; |
576 | |
577 | return qMax<qint64>(a: 0, b: readBufferMaxSize - buffer.size()); |
578 | } |
579 | |
580 | void QNetworkReplyImplPrivate::initCacheSaveDevice() |
581 | { |
582 | Q_Q(QNetworkReplyImpl); |
583 | |
584 | // The disk cache does not support partial content, so don't even try to |
585 | // save any such content into the cache. |
586 | if (q->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt() == 206) { |
587 | cacheEnabled = false; |
588 | return; |
589 | } |
590 | |
591 | // save the meta data |
592 | QNetworkCacheMetaData metaData; |
593 | metaData.setUrl(url); |
594 | metaData = backend->fetchCacheMetaData(metaData); |
595 | |
596 | // save the redirect request also in the cache |
597 | QVariant redirectionTarget = q->attribute(code: QNetworkRequest::RedirectionTargetAttribute); |
598 | if (redirectionTarget.isValid()) { |
599 | QNetworkCacheMetaData::AttributesMap attributes = metaData.attributes(); |
600 | attributes.insert(key: QNetworkRequest::RedirectionTargetAttribute, value: redirectionTarget); |
601 | metaData.setAttributes(attributes); |
602 | } |
603 | |
604 | cacheSaveDevice = networkCache()->prepare(metaData); |
605 | |
606 | if (!cacheSaveDevice || (cacheSaveDevice && !cacheSaveDevice->isOpen())) { |
607 | if (Q_UNLIKELY(cacheSaveDevice && !cacheSaveDevice->isOpen())) |
608 | qCritical(msg: "QNetworkReplyImpl: network cache returned a device that is not open -- " |
609 | "class %s probably needs to be fixed" , |
610 | networkCache()->metaObject()->className()); |
611 | |
612 | networkCache()->remove(url); |
613 | cacheSaveDevice = nullptr; |
614 | cacheEnabled = false; |
615 | } |
616 | } |
617 | |
618 | // we received downstream data and send this to the cache |
619 | // and to our buffer (which in turn gets read by the user of QNetworkReply) |
620 | void QNetworkReplyImplPrivate::appendDownstreamData(QByteDataBuffer &data) |
621 | { |
622 | Q_Q(QNetworkReplyImpl); |
623 | if (!q->isOpen()) |
624 | return; |
625 | |
626 | if (cacheEnabled && !cacheSaveDevice) { |
627 | initCacheSaveDevice(); |
628 | } |
629 | |
630 | qint64 bytesWritten = 0; |
631 | for (int i = 0; i < data.bufferCount(); i++) { |
632 | QByteArray const &item = data[i]; |
633 | |
634 | if (cacheSaveDevice) |
635 | cacheSaveDevice->write(data: item.constData(), len: item.size()); |
636 | buffer.append(qba: item); |
637 | |
638 | bytesWritten += item.size(); |
639 | } |
640 | data.clear(); |
641 | |
642 | bytesDownloaded += bytesWritten; |
643 | lastBytesDownloaded = bytesDownloaded; |
644 | |
645 | appendDownstreamDataSignalEmissions(); |
646 | } |
647 | |
648 | void QNetworkReplyImplPrivate::appendDownstreamDataSignalEmissions() |
649 | { |
650 | Q_Q(QNetworkReplyImpl); |
651 | |
652 | QVariant totalSize = cookedHeaders.value(key: QNetworkRequest::ContentLengthHeader); |
653 | if (preMigrationDownloaded != Q_INT64_C(-1)) |
654 | totalSize = totalSize.toLongLong() + preMigrationDownloaded; |
655 | pauseNotificationHandling(); |
656 | // important: At the point of this readyRead(), the data parameter list must be empty, |
657 | // else implicit sharing will trigger memcpy when the user is reading data! |
658 | emit q->readyRead(); |
659 | // emit readyRead before downloadProgress incase this will cause events to be |
660 | // processed and we get into a recursive call (as in QProgressDialog). |
661 | if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
662 | downloadProgressSignalChoke.restart(); |
663 | emit q->downloadProgress(bytesReceived: bytesDownloaded, |
664 | bytesTotal: totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); |
665 | } |
666 | |
667 | resumeNotificationHandling(); |
668 | // do we still have room in the buffer? |
669 | if (nextDownstreamBlockSize() > 0) |
670 | backendNotify(notification: QNetworkReplyImplPrivate::NotifyDownstreamReadyWrite); |
671 | } |
672 | |
673 | // this is used when it was fetched from the cache, right? |
674 | void QNetworkReplyImplPrivate::appendDownstreamData(QIODevice *data) |
675 | { |
676 | Q_Q(QNetworkReplyImpl); |
677 | if (!q->isOpen()) |
678 | return; |
679 | |
680 | // read until EOF from data |
681 | if (Q_UNLIKELY(copyDevice)) { |
682 | qCritical(msg: "QNetworkReplyImpl: copy from QIODevice already in progress -- " |
683 | "backend probly needs to be fixed" ); |
684 | return; |
685 | } |
686 | |
687 | copyDevice = data; |
688 | q->connect(asender: copyDevice, SIGNAL(readyRead()), SLOT(_q_copyReadyRead())); |
689 | q->connect(asender: copyDevice, SIGNAL(readChannelFinished()), SLOT(_q_copyReadChannelFinished())); |
690 | |
691 | // start the copy: |
692 | _q_copyReadyRead(); |
693 | } |
694 | |
695 | void QNetworkReplyImplPrivate::appendDownstreamData(const QByteArray &data) |
696 | { |
697 | Q_UNUSED(data) |
698 | // TODO implement |
699 | |
700 | // TODO call |
701 | |
702 | qFatal(msg: "QNetworkReplyImplPrivate::appendDownstreamData not implemented" ); |
703 | } |
704 | |
705 | static void downloadBufferDeleter(char *ptr) |
706 | { |
707 | delete[] ptr; |
708 | } |
709 | |
710 | char* QNetworkReplyImplPrivate::getDownloadBuffer(qint64 size) |
711 | { |
712 | Q_Q(QNetworkReplyImpl); |
713 | |
714 | if (!downloadBuffer) { |
715 | // We are requested to create it |
716 | // Check attribute() if allocating a buffer of that size can be allowed |
717 | QVariant bufferAllocationPolicy = request.attribute(code: QNetworkRequest::MaximumDownloadBufferSizeAttribute); |
718 | if (bufferAllocationPolicy.isValid() && bufferAllocationPolicy.toLongLong() >= size) { |
719 | downloadBufferCurrentSize = 0; |
720 | downloadBufferMaximumSize = size; |
721 | downloadBuffer = new char[downloadBufferMaximumSize]; // throws if allocation fails |
722 | downloadBufferPointer = QSharedPointer<char>(downloadBuffer, downloadBufferDeleter); |
723 | |
724 | q->setAttribute(code: QNetworkRequest::DownloadBufferAttribute, value: QVariant::fromValue<QSharedPointer<char> > (value: downloadBufferPointer)); |
725 | } |
726 | } |
727 | |
728 | return downloadBuffer; |
729 | } |
730 | |
731 | void QNetworkReplyImplPrivate::setDownloadBuffer(QSharedPointer<char> sp, qint64 size) |
732 | { |
733 | Q_Q(QNetworkReplyImpl); |
734 | |
735 | downloadBufferPointer = sp; |
736 | downloadBuffer = downloadBufferPointer.data(); |
737 | downloadBufferCurrentSize = 0; |
738 | downloadBufferMaximumSize = size; |
739 | q->setAttribute(code: QNetworkRequest::DownloadBufferAttribute, value: QVariant::fromValue<QSharedPointer<char> > (value: downloadBufferPointer)); |
740 | } |
741 | |
742 | |
743 | void QNetworkReplyImplPrivate::appendDownstreamDataDownloadBuffer(qint64 bytesReceived, qint64 bytesTotal) |
744 | { |
745 | Q_Q(QNetworkReplyImpl); |
746 | if (!q->isOpen()) |
747 | return; |
748 | |
749 | if (cacheEnabled && !cacheSaveDevice) |
750 | initCacheSaveDevice(); |
751 | |
752 | if (cacheSaveDevice && bytesReceived == bytesTotal) { |
753 | // if (lastBytesDownloaded == -1) |
754 | // lastBytesDownloaded = 0; |
755 | // cacheSaveDevice->write(downloadBuffer + lastBytesDownloaded, bytesReceived - lastBytesDownloaded); |
756 | |
757 | // Write everything in one go if we use a download buffer. might be more performant. |
758 | cacheSaveDevice->write(data: downloadBuffer, len: bytesTotal); |
759 | } |
760 | |
761 | bytesDownloaded = bytesReceived; |
762 | lastBytesDownloaded = bytesReceived; |
763 | |
764 | downloadBufferCurrentSize = bytesReceived; |
765 | |
766 | // Only emit readyRead when actual data is there |
767 | // emit readyRead before downloadProgress incase this will cause events to be |
768 | // processed and we get into a recursive call (as in QProgressDialog). |
769 | if (bytesDownloaded > 0) |
770 | emit q->readyRead(); |
771 | if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { |
772 | downloadProgressSignalChoke.restart(); |
773 | emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal); |
774 | } |
775 | } |
776 | |
777 | void QNetworkReplyImplPrivate::finished() |
778 | { |
779 | Q_Q(QNetworkReplyImpl); |
780 | |
781 | if (state == Finished || state == Aborted || state == WaitingForSession) |
782 | return; |
783 | |
784 | pauseNotificationHandling(); |
785 | QVariant totalSize = cookedHeaders.value(key: QNetworkRequest::ContentLengthHeader); |
786 | if (preMigrationDownloaded != Q_INT64_C(-1)) |
787 | totalSize = totalSize.toLongLong() + preMigrationDownloaded; |
788 | |
789 | if (!manager.isNull()) { |
790 | #ifndef QT_NO_BEARERMANAGEMENT // ### Qt6: Remove section |
791 | QSharedPointer<QNetworkSession> session (manager->d_func()->getNetworkSession()); |
792 | if (session && session->state() == QNetworkSession::Roaming && |
793 | state == Working && errorCode != QNetworkReply::OperationCanceledError) { |
794 | // only content with a known size will fail with a temporary network failure error |
795 | if (!totalSize.isNull()) { |
796 | if (bytesDownloaded != totalSize) { |
797 | if (migrateBackend()) { |
798 | // either we are migrating or the request is finished/aborted |
799 | if (state == Reconnecting || state == WaitingForSession) { |
800 | resumeNotificationHandling(); |
801 | return; // exit early if we are migrating. |
802 | } |
803 | } else { |
804 | error(code: QNetworkReply::TemporaryNetworkFailureError, |
805 | errorString: QNetworkReply::tr(s: "Temporary network failure." )); |
806 | } |
807 | } |
808 | } |
809 | } |
810 | #endif |
811 | } |
812 | resumeNotificationHandling(); |
813 | |
814 | state = Finished; |
815 | q->setFinished(true); |
816 | |
817 | pendingNotifications.clear(); |
818 | |
819 | pauseNotificationHandling(); |
820 | if (totalSize.isNull() || totalSize == -1) { |
821 | emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: bytesDownloaded); |
822 | } else { |
823 | emit q->downloadProgress(bytesReceived: bytesDownloaded, bytesTotal: totalSize.toLongLong()); |
824 | } |
825 | |
826 | if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer)) |
827 | emit q->uploadProgress(bytesSent: 0, bytesTotal: 0); |
828 | resumeNotificationHandling(); |
829 | |
830 | // if we don't know the total size of or we received everything save the cache |
831 | if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize) |
832 | completeCacheSave(); |
833 | |
834 | // note: might not be a good idea, since users could decide to delete us |
835 | // which would delete the backend too... |
836 | // maybe we should protect the backend |
837 | pauseNotificationHandling(); |
838 | emit q->readChannelFinished(); |
839 | emit q->finished(); |
840 | resumeNotificationHandling(); |
841 | } |
842 | |
843 | void QNetworkReplyImplPrivate::error(QNetworkReplyImpl::NetworkError code, const QString &errorMessage) |
844 | { |
845 | Q_Q(QNetworkReplyImpl); |
846 | // Can't set and emit multiple errors. |
847 | if (errorCode != QNetworkReply::NoError) { |
848 | qWarning( msg: "QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once." ); |
849 | return; |
850 | } |
851 | |
852 | errorCode = code; |
853 | q->setErrorString(errorMessage); |
854 | |
855 | // note: might not be a good idea, since users could decide to delete us |
856 | // which would delete the backend too... |
857 | // maybe we should protect the backend |
858 | emit q->errorOccurred(code); |
859 | } |
860 | |
861 | void QNetworkReplyImplPrivate::metaDataChanged() |
862 | { |
863 | Q_Q(QNetworkReplyImpl); |
864 | // 1. do we have cookies? |
865 | // 2. are we allowed to set them? |
866 | if (!manager.isNull()) { |
867 | const auto it = cookedHeaders.constFind(key: QNetworkRequest::SetCookieHeader); |
868 | if (it != cookedHeaders.cend() |
869 | && request.attribute(code: QNetworkRequest::CookieSaveControlAttribute, |
870 | defaultValue: QNetworkRequest::Automatic).toInt() == QNetworkRequest::Automatic) { |
871 | QNetworkCookieJar *jar = manager->cookieJar(); |
872 | if (jar) { |
873 | QList<QNetworkCookie> cookies = |
874 | qvariant_cast<QList<QNetworkCookie> >(v: it.value()); |
875 | jar->setCookiesFromUrl(cookieList: cookies, url); |
876 | } |
877 | } |
878 | } |
879 | |
880 | emit q->metaDataChanged(); |
881 | } |
882 | |
883 | void QNetworkReplyImplPrivate::redirectionRequested(const QUrl &target) |
884 | { |
885 | attributes.insert(key: QNetworkRequest::RedirectionTargetAttribute, value: target); |
886 | } |
887 | |
888 | void QNetworkReplyImplPrivate::encrypted() |
889 | { |
890 | #ifndef QT_NO_SSL |
891 | Q_Q(QNetworkReplyImpl); |
892 | emit q->encrypted(); |
893 | #endif |
894 | } |
895 | |
896 | void QNetworkReplyImplPrivate::sslErrors(const QList<QSslError> &errors) |
897 | { |
898 | #ifndef QT_NO_SSL |
899 | Q_Q(QNetworkReplyImpl); |
900 | emit q->sslErrors(errors); |
901 | #else |
902 | Q_UNUSED(errors); |
903 | #endif |
904 | } |
905 | |
906 | QNetworkReplyImpl::QNetworkReplyImpl(QObject *parent) |
907 | : QNetworkReply(*new QNetworkReplyImplPrivate, parent) |
908 | { |
909 | } |
910 | |
911 | QNetworkReplyImpl::~QNetworkReplyImpl() |
912 | { |
913 | Q_D(QNetworkReplyImpl); |
914 | |
915 | // This code removes the data from the cache if it was prematurely aborted. |
916 | // See QNetworkReplyImplPrivate::completeCacheSave(), we disable caching there after the cache |
917 | // save had been properly finished. So if it is still enabled it means we got deleted/aborted. |
918 | if (d->isCachingEnabled()) |
919 | d->networkCache()->remove(url: url()); |
920 | } |
921 | |
922 | void QNetworkReplyImpl::abort() |
923 | { |
924 | Q_D(QNetworkReplyImpl); |
925 | if (d->state == QNetworkReplyPrivate::Finished || d->state == QNetworkReplyPrivate::Aborted) |
926 | return; |
927 | |
928 | // stop both upload and download |
929 | if (d->outgoingData) |
930 | disconnect(sender: d->outgoingData, signal: nullptr, receiver: this, member: nullptr); |
931 | if (d->copyDevice) |
932 | disconnect(sender: d->copyDevice, signal: nullptr, receiver: this, member: nullptr); |
933 | |
934 | QNetworkReply::close(); |
935 | |
936 | // call finished which will emit signals |
937 | d->error(code: OperationCanceledError, errorMessage: tr(s: "Operation canceled" )); |
938 | if (d->state == QNetworkReplyPrivate::WaitingForSession) |
939 | d->state = QNetworkReplyPrivate::Working; |
940 | d->finished(); |
941 | d->state = QNetworkReplyPrivate::Aborted; |
942 | |
943 | // finished may access the backend |
944 | if (d->backend) { |
945 | d->backend->deleteLater(); |
946 | d->backend = nullptr; |
947 | } |
948 | } |
949 | |
950 | void QNetworkReplyImpl::close() |
951 | { |
952 | Q_D(QNetworkReplyImpl); |
953 | if (d->state == QNetworkReplyPrivate::Aborted || |
954 | d->state == QNetworkReplyPrivate::Finished) |
955 | return; |
956 | |
957 | // stop the download |
958 | if (d->backend) |
959 | d->backend->closeDownstreamChannel(); |
960 | if (d->copyDevice) |
961 | disconnect(sender: d->copyDevice, signal: nullptr, receiver: this, member: nullptr); |
962 | |
963 | QNetworkReply::close(); |
964 | |
965 | // call finished which will emit signals |
966 | d->error(code: OperationCanceledError, errorMessage: tr(s: "Operation canceled" )); |
967 | d->finished(); |
968 | } |
969 | |
970 | /*! |
971 | Returns the number of bytes available for reading with |
972 | QIODevice::read(). The number of bytes available may grow until |
973 | the finished() signal is emitted. |
974 | */ |
975 | qint64 QNetworkReplyImpl::bytesAvailable() const |
976 | { |
977 | // Special case for the "zero copy" download buffer |
978 | Q_D(const QNetworkReplyImpl); |
979 | if (d->downloadBuffer) { |
980 | qint64 maxAvail = d->downloadBufferCurrentSize - d->downloadBufferReadPosition; |
981 | return QNetworkReply::bytesAvailable() + maxAvail; |
982 | } |
983 | |
984 | return QNetworkReply::bytesAvailable(); |
985 | } |
986 | |
987 | void QNetworkReplyImpl::setReadBufferSize(qint64 size) |
988 | { |
989 | Q_D(QNetworkReplyImpl); |
990 | if (size > d->readBufferMaxSize && |
991 | size > d->buffer.size()) |
992 | d->backendNotify(notification: QNetworkReplyImplPrivate::NotifyDownstreamReadyWrite); |
993 | |
994 | QNetworkReply::setReadBufferSize(size); |
995 | |
996 | if (d->backend) |
997 | d->backend->setDownstreamLimited(d->readBufferMaxSize > 0); |
998 | } |
999 | |
1000 | #ifndef QT_NO_SSL |
1001 | void QNetworkReplyImpl::sslConfigurationImplementation(QSslConfiguration &configuration) const |
1002 | { |
1003 | Q_D(const QNetworkReplyImpl); |
1004 | if (d->backend) |
1005 | d->backend->fetchSslConfiguration(configuration); |
1006 | } |
1007 | |
1008 | void QNetworkReplyImpl::setSslConfigurationImplementation(const QSslConfiguration &config) |
1009 | { |
1010 | Q_D(QNetworkReplyImpl); |
1011 | if (d->backend && !config.isNull()) |
1012 | d->backend->setSslConfiguration(config); |
1013 | } |
1014 | |
1015 | void QNetworkReplyImpl::ignoreSslErrors() |
1016 | { |
1017 | Q_D(QNetworkReplyImpl); |
1018 | if (d->backend) |
1019 | d->backend->ignoreSslErrors(); |
1020 | } |
1021 | |
1022 | void QNetworkReplyImpl::ignoreSslErrorsImplementation(const QList<QSslError> &errors) |
1023 | { |
1024 | Q_D(QNetworkReplyImpl); |
1025 | if (d->backend) |
1026 | d->backend->ignoreSslErrors(errors); |
1027 | } |
1028 | #endif // QT_NO_SSL |
1029 | |
1030 | /*! |
1031 | \internal |
1032 | */ |
1033 | qint64 QNetworkReplyImpl::readData(char *data, qint64 maxlen) |
1034 | { |
1035 | Q_D(QNetworkReplyImpl); |
1036 | |
1037 | // Special case code if we have the "zero copy" download buffer |
1038 | if (d->downloadBuffer) { |
1039 | qint64 maxAvail = qMin<qint64>(a: d->downloadBufferCurrentSize - d->downloadBufferReadPosition, b: maxlen); |
1040 | if (maxAvail == 0) |
1041 | return d->state == QNetworkReplyPrivate::Finished ? -1 : 0; |
1042 | // FIXME what about "Aborted" state? |
1043 | memcpy(dest: data, src: d->downloadBuffer + d->downloadBufferReadPosition, n: maxAvail); |
1044 | d->downloadBufferReadPosition += maxAvail; |
1045 | return maxAvail; |
1046 | } |
1047 | |
1048 | |
1049 | // FIXME what about "Aborted" state? |
1050 | if (d->state == QNetworkReplyPrivate::Finished) |
1051 | return -1; |
1052 | |
1053 | d->backendNotify(notification: QNetworkReplyImplPrivate::NotifyDownstreamReadyWrite); |
1054 | return 0; |
1055 | } |
1056 | |
1057 | /*! |
1058 | \internal Reimplemented for internal purposes |
1059 | */ |
1060 | bool QNetworkReplyImpl::event(QEvent *e) |
1061 | { |
1062 | if (e->type() == QEvent::NetworkReplyUpdated) { |
1063 | d_func()->handleNotifications(); |
1064 | return true; |
1065 | } |
1066 | |
1067 | return QObject::event(event: e); |
1068 | } |
1069 | |
1070 | /* |
1071 | Migrates the backend of the QNetworkReply to a new network connection if required. Returns |
1072 | true if the reply is migrated or it is not required; otherwise returns \c false. |
1073 | */ |
1074 | bool QNetworkReplyImplPrivate::migrateBackend() |
1075 | { |
1076 | Q_Q(QNetworkReplyImpl); |
1077 | |
1078 | // Network reply is already finished or aborted, don't need to migrate. |
1079 | if (state == Finished || state == Aborted) |
1080 | return true; |
1081 | |
1082 | // Request has outgoing data, not migrating. |
1083 | if (outgoingData) |
1084 | return false; |
1085 | |
1086 | // Request is serviced from the cache, don't need to migrate. |
1087 | if (copyDevice) |
1088 | return true; |
1089 | |
1090 | // Backend does not support resuming download. |
1091 | if (backend && !backend->canResume()) |
1092 | return false; |
1093 | |
1094 | state = QNetworkReplyPrivate::Reconnecting; |
1095 | |
1096 | cookedHeaders.clear(); |
1097 | rawHeaders.clear(); |
1098 | |
1099 | preMigrationDownloaded = bytesDownloaded; |
1100 | |
1101 | delete backend; |
1102 | backend = manager->d_func()->findBackend(op: operation, request); |
1103 | |
1104 | if (backend) { |
1105 | backend->setParent(q); |
1106 | backend->reply = this; |
1107 | backend->setResumeOffset(bytesDownloaded); |
1108 | } |
1109 | |
1110 | QMetaObject::invokeMethod(obj: q, member: "_q_startOperation" , type: Qt::QueuedConnection); |
1111 | |
1112 | return true; |
1113 | } |
1114 | |
1115 | QT_END_NAMESPACE |
1116 | |
1117 | #include "moc_qnetworkreplyimpl_p.cpp" |
1118 | |
1119 | |