1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4 SPDX-FileCopyrightText: 2000-2013 David Faure <faure@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "transferjob.h"
10#include "job_p.h"
11#include "worker_p.h"
12#include <QDebug>
13#include <kurlauthorized.h>
14
15using namespace KIO;
16
17static const int MAX_READ_BUF_SIZE = (64 * 1024); // 64 KB at a time seems reasonable...
18
19TransferJob::TransferJob(TransferJobPrivate &dd)
20 : SimpleJob(dd)
21{
22 Q_D(TransferJob);
23 if (d->m_command == CMD_PUT) {
24 d->m_extraFlags |= JobPrivate::EF_TransferJobDataSent;
25 }
26
27 if (d->m_outgoingDataSource) {
28 d->m_readChannelFinishedConnection = connect(sender: d->m_outgoingDataSource, signal: &QIODevice::readChannelFinished, context: this, slot: [d]() {
29 d->slotIODeviceClosedBeforeStart();
30 });
31 }
32}
33
34TransferJob::~TransferJob()
35{
36}
37
38// Worker sends data
39void TransferJob::slotData(const QByteArray &_data)
40{
41 Q_D(TransferJob);
42 if (d->m_command == CMD_GET && !d->m_isMimetypeEmitted) {
43 qCWarning(KIO_CORE) << "mimeType() not emitted when sending first data!; job URL =" << d->m_url << "data size =" << _data.size();
44 }
45 // shut up the warning, HACK: downside is that it changes the meaning of the variable
46 d->m_isMimetypeEmitted = true;
47
48 if (d->m_redirectionURL.isEmpty() || !d->m_redirectionURL.isValid() || error()) {
49 Q_EMIT data(job: this, data: _data);
50 }
51}
52
53void KIO::TransferJob::setTotalSize(KIO::filesize_t bytes)
54{
55 setTotalAmount(unit: KJob::Bytes, amount: bytes);
56}
57
58// Worker got a redirection request
59void TransferJob::slotRedirection(const QUrl &url)
60{
61 Q_D(TransferJob);
62 // qDebug() << url;
63 if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), baseUrl: d->m_url, destUrl: url)) {
64 qCWarning(KIO_CORE) << "Redirection from" << d->m_url << "to" << url << "REJECTED!";
65 return;
66 }
67
68 // Some websites keep redirecting to themselves where each redirection
69 // acts as the stage in a state-machine. We define "endless redirections"
70 // as 5 redirections to the same URL.
71 if (d->m_redirectionList.count(t: url) > 5) {
72 // qDebug() << "CYCLIC REDIRECTION!";
73 setError(ERR_CYCLIC_LINK);
74 setErrorText(d->m_url.toDisplayString());
75 } else {
76 d->m_redirectionURL = url; // We'll remember that when the job finishes
77 d->m_redirectionList.append(t: url);
78 QString sslInUse = queryMetaData(QStringLiteral("ssl_in_use"));
79 if (!sslInUse.isNull()) { // the key is present
80 addMetaData(QStringLiteral("ssl_was_in_use"), value: sslInUse);
81 } else {
82 addMetaData(QStringLiteral("ssl_was_in_use"), QStringLiteral("FALSE"));
83 }
84 // Tell the user that we haven't finished yet
85 Q_EMIT redirection(job: this, url: d->m_redirectionURL);
86 }
87}
88
89void TransferJob::slotFinished()
90{
91 Q_D(TransferJob);
92
93 // qDebug() << d->m_url;
94 if (!d->m_redirectionURL.isEmpty() && d->m_redirectionURL.isValid()) {
95 // qDebug() << "Redirection to" << m_redirectionURL;
96 if (queryMetaData(QStringLiteral("permanent-redirect")) == QLatin1String("true")) {
97 Q_EMIT permanentRedirection(job: this, fromUrl: d->m_url, toUrl: d->m_redirectionURL);
98 }
99
100 if (queryMetaData(QStringLiteral("redirect-to-get")) == QLatin1String("true")) {
101 d->m_command = CMD_GET;
102 d->m_outgoingMetaData.remove(QStringLiteral("content-type"));
103 }
104
105 if (d->m_redirectionHandlingEnabled) {
106 // Honour the redirection
107 // We take the approach of "redirecting this same job"
108 // Another solution would be to create a subjob, but the same problem
109 // happens (unpacking+repacking)
110 d->staticData.truncate(pos: 0);
111 d->m_incomingMetaData.clear();
112 if (queryMetaData(QStringLiteral("cache")) != QLatin1String("reload")) {
113 addMetaData(QStringLiteral("cache"), QStringLiteral("refresh"));
114 }
115 d->m_internalSuspended = false;
116 // The very tricky part is the packed arguments business
117 QUrl dummyUrl;
118 QDataStream istream(d->m_packedArgs);
119 switch (d->m_command) {
120 case CMD_GET:
121 case CMD_STAT:
122 case CMD_DEL: {
123 d->m_packedArgs.truncate(pos: 0);
124 QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly);
125 stream << d->m_redirectionURL;
126 break;
127 }
128 case CMD_PUT: {
129 int permissions;
130 qint8 iOverwrite;
131 qint8 iResume;
132 istream >> dummyUrl >> iOverwrite >> iResume >> permissions;
133 d->m_packedArgs.truncate(pos: 0);
134 QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly);
135 stream << d->m_redirectionURL << iOverwrite << iResume << permissions;
136 break;
137 }
138 case CMD_SPECIAL: {
139 int specialcmd;
140 istream >> specialcmd;
141 if (specialcmd == 1) { // HTTP POST
142 d->m_outgoingMetaData.remove(QStringLiteral("content-type"));
143 addMetaData(QStringLiteral("cache"), QStringLiteral("reload"));
144 d->m_packedArgs.truncate(pos: 0);
145 QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly);
146 stream << d->m_redirectionURL;
147 d->m_command = CMD_GET;
148 }
149 break;
150 }
151 }
152 d->restartAfterRedirection(redirectionUrl: &d->m_redirectionURL);
153 return;
154 }
155 }
156
157 SimpleJob::slotFinished();
158}
159
160void TransferJob::setAsyncDataEnabled(bool enabled)
161{
162 Q_D(TransferJob);
163 if (enabled) {
164 d->m_extraFlags |= JobPrivate::EF_TransferJobAsync;
165 } else {
166 d->m_extraFlags &= ~JobPrivate::EF_TransferJobAsync;
167 }
168}
169
170void TransferJob::sendAsyncData(const QByteArray &dataForWorker)
171{
172 Q_D(TransferJob);
173 if (d->m_extraFlags & JobPrivate::EF_TransferJobNeedData) {
174 if (d->m_worker) {
175 d->m_worker->send(cmd: MSG_DATA, arr: dataForWorker);
176 }
177 if (d->m_extraFlags & JobPrivate::EF_TransferJobDataSent) { // put job -> emit progress
178 KIO::filesize_t size = processedAmount(unit: KJob::Bytes) + dataForWorker.size();
179 setProcessedAmount(unit: KJob::Bytes, amount: size);
180 }
181 }
182
183 d->m_extraFlags &= ~JobPrivate::EF_TransferJobNeedData;
184}
185
186QString TransferJob::mimetype() const
187{
188 return d_func()->m_mimetype;
189}
190
191QUrl TransferJob::redirectUrl() const
192{
193 return d_func()->m_redirectionURL;
194}
195
196// Worker requests data
197void TransferJob::slotDataReq()
198{
199 Q_D(TransferJob);
200 QByteArray dataForWorker;
201
202 d->m_extraFlags |= JobPrivate::EF_TransferJobNeedData;
203
204 if (!d->staticData.isEmpty()) {
205 dataForWorker = d->staticData;
206 d->staticData.clear();
207 } else {
208 Q_EMIT dataReq(job: this, data&: dataForWorker);
209
210 if (d->m_extraFlags & JobPrivate::EF_TransferJobAsync) {
211 return;
212 }
213 }
214
215 static const int max_size = 14 * 1024 * 1024;
216 if (dataForWorker.size() > max_size) {
217 // qDebug() << "send" << dataForWorker.size() / 1024 / 1024 << "MB of data in TransferJob::dataReq. This needs to be split, which requires a copy. Fix
218 // the application.";
219 d->staticData = QByteArray(dataForWorker.data() + max_size, dataForWorker.size() - max_size);
220 dataForWorker.truncate(pos: max_size);
221 }
222
223 sendAsyncData(dataForWorker);
224}
225
226void TransferJob::slotMimetype(const QString &type)
227{
228 Q_D(TransferJob);
229 d->m_mimetype = type;
230 if (d->m_command == CMD_GET && d->m_isMimetypeEmitted) {
231 qCWarning(KIO_CORE) << "mimetype() emitted again, or after sending first data!; job URL =" << d->m_url;
232 }
233 d->m_isMimetypeEmitted = true;
234 Q_EMIT mimeTypeFound(job: this, mimeType: type);
235}
236
237void TransferJobPrivate::internalSuspend()
238{
239 m_internalSuspended = true;
240 if (m_worker) {
241 m_worker->suspend();
242 }
243}
244
245void TransferJobPrivate::internalResume()
246{
247 m_internalSuspended = false;
248 if (m_worker && !q_func()->isSuspended()) {
249 m_worker->resume();
250 }
251}
252
253bool TransferJob::doResume()
254{
255 Q_D(TransferJob);
256 if (!SimpleJob::doResume()) {
257 return false;
258 }
259 if (d->m_internalSuspended) {
260 d->internalSuspend();
261 }
262 return true;
263}
264
265bool TransferJob::isErrorPage() const
266{
267 return d_func()->m_errorPage;
268}
269
270void TransferJobPrivate::start(Worker *worker)
271{
272 Q_Q(TransferJob);
273 Q_ASSERT(worker);
274 JobPrivate::emitTransferring(q, url: m_url);
275 q->connect(sender: worker, signal: &WorkerInterface::data, context: q, slot: &TransferJob::slotData);
276
277 if (m_outgoingDataSource) {
278 if (m_extraFlags & JobPrivate::EF_TransferJobAsync) {
279 auto dataReqFunc = [this]() {
280 slotDataReqFromDevice();
281 };
282 q->connect(sender: m_outgoingDataSource, signal: &QIODevice::readyRead, context: q, slot&: dataReqFunc);
283 auto ioClosedFunc = [this]() {
284 slotIODeviceClosed();
285 };
286 q->connect(sender: m_outgoingDataSource, signal: &QIODevice::readChannelFinished, context: q, slot&: ioClosedFunc);
287 // We don't really need to disconnect since we're never checking
288 // m_closedBeforeStart again but it's the proper thing to do logically
289 QObject::disconnect(m_readChannelFinishedConnection);
290 if (m_closedBeforeStart) {
291 QMetaObject::invokeMethod(object: q, function&: ioClosedFunc, type: Qt::QueuedConnection);
292 } else if (m_outgoingDataSource->bytesAvailable() > 0) {
293 QMetaObject::invokeMethod(object: q, function&: dataReqFunc, type: Qt::QueuedConnection);
294 }
295 } else {
296 q->connect(sender: worker, signal: &WorkerInterface::dataReq, context: q, slot: [this]() {
297 slotDataReqFromDevice();
298 });
299 }
300 } else {
301 q->connect(sender: worker, signal: &WorkerInterface::dataReq, context: q, slot: &TransferJob::slotDataReq);
302 }
303
304 q->connect(sender: worker, signal: &WorkerInterface::redirection, context: q, slot: &TransferJob::slotRedirection);
305
306 q->connect(sender: worker, signal: &WorkerInterface::mimeType, context: q, slot: &TransferJob::slotMimetype);
307
308 q->connect(sender: worker, signal: &WorkerInterface::errorPage, context: q, slot: [this]() {
309 m_errorPage = true;
310 });
311
312 q->connect(sender: worker, signal: &WorkerInterface::canResume, context: q, slot: [q](KIO::filesize_t offset) {
313 Q_EMIT q->canResume(job: q, offset);
314 });
315
316 if (worker->suspended()) {
317 m_mimetype = QStringLiteral("unknown");
318 // WABA: The worker was put on hold. Resume operation.
319 worker->resume();
320 }
321
322 SimpleJobPrivate::start(worker);
323 if (m_internalSuspended) {
324 worker->suspend();
325 }
326}
327
328void TransferJobPrivate::slotDataReqFromDevice()
329{
330 Q_Q(TransferJob);
331
332 bool done = false;
333 QByteArray dataForWorker;
334
335 m_extraFlags |= JobPrivate::EF_TransferJobNeedData;
336
337 if (m_outgoingDataSource) {
338 dataForWorker.resize(size: MAX_READ_BUF_SIZE);
339
340 // Code inspired in QNonContiguousByteDevice
341 qint64 bytesRead = m_outgoingDataSource->read(data: dataForWorker.data(), maxlen: MAX_READ_BUF_SIZE);
342 if (bytesRead >= 0) {
343 dataForWorker.resize(size: bytesRead);
344 } else {
345 dataForWorker.clear();
346 }
347 done = ((bytesRead == -1) || (bytesRead == 0 && m_outgoingDataSource->atEnd() && !m_outgoingDataSource->isSequential()));
348 }
349
350 if (dataForWorker.isEmpty()) {
351 Q_EMIT q->dataReq(job: q, data&: dataForWorker);
352 if (!done && (m_extraFlags & JobPrivate::EF_TransferJobAsync)) {
353 return;
354 }
355 }
356
357 q->sendAsyncData(dataForWorker);
358}
359
360void TransferJobPrivate::slotIODeviceClosedBeforeStart()
361{
362 m_closedBeforeStart = true;
363}
364
365void TransferJobPrivate::slotIODeviceClosed()
366{
367 Q_Q(TransferJob);
368 const QByteArray remainder = m_outgoingDataSource->readAll();
369 if (!remainder.isEmpty()) {
370 m_extraFlags |= JobPrivate::EF_TransferJobNeedData;
371 q->sendAsyncData(dataForWorker: remainder);
372 }
373
374 m_extraFlags |= JobPrivate::EF_TransferJobNeedData;
375
376 // We send an empty data array to indicate the stream is over
377 q->sendAsyncData(dataForWorker: QByteArray());
378}
379
380void TransferJob::setModificationTime(const QDateTime &mtime)
381{
382 addMetaData(QStringLiteral("modified"), value: mtime.toString(format: Qt::ISODate));
383}
384
385TransferJob *KIO::get(const QUrl &url, LoadType reload, JobFlags flags)
386{
387 // Send decoded path and encoded query
388 KIO_ARGS << url;
389 TransferJob *job = TransferJobPrivate::newJob(url, command: CMD_GET, packedArgs, staticData: QByteArray(), flags);
390 if (reload == Reload) {
391 job->addMetaData(QStringLiteral("cache"), QStringLiteral("reload"));
392 }
393 return job;
394}
395
396#include "moc_transferjob.cpp"
397

source code of kio/src/core/transferjob.cpp