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 | |
15 | using namespace KIO; |
16 | |
17 | static const int MAX_READ_BUF_SIZE = (64 * 1024); // 64 KB at a time seems reasonable... |
18 | |
19 | TransferJob::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 | |
34 | TransferJob::~TransferJob() |
35 | { |
36 | } |
37 | |
38 | // Worker sends data |
39 | void 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 | |
53 | void KIO::TransferJob::setTotalSize(KIO::filesize_t bytes) |
54 | { |
55 | setTotalAmount(unit: KJob::Bytes, amount: bytes); |
56 | } |
57 | |
58 | // Worker got a redirection request |
59 | void 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 | |
89 | void 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 | |
160 | void 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 | |
170 | void 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 | |
186 | QString TransferJob::mimetype() const |
187 | { |
188 | return d_func()->m_mimetype; |
189 | } |
190 | |
191 | QUrl TransferJob::redirectUrl() const |
192 | { |
193 | return d_func()->m_redirectionURL; |
194 | } |
195 | |
196 | // Worker requests data |
197 | void 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 | |
226 | void 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 | |
237 | void TransferJobPrivate::internalSuspend() |
238 | { |
239 | m_internalSuspended = true; |
240 | if (m_worker) { |
241 | m_worker->suspend(); |
242 | } |
243 | } |
244 | |
245 | void TransferJobPrivate::internalResume() |
246 | { |
247 | m_internalSuspended = false; |
248 | if (m_worker && !q_func()->isSuspended()) { |
249 | m_worker->resume(); |
250 | } |
251 | } |
252 | |
253 | bool 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 | |
265 | bool TransferJob::isErrorPage() const |
266 | { |
267 | return d_func()->m_errorPage; |
268 | } |
269 | |
270 | void 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 | |
328 | void 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 | |
360 | void TransferJobPrivate::slotIODeviceClosedBeforeStart() |
361 | { |
362 | m_closedBeforeStart = true; |
363 | } |
364 | |
365 | void 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 | |
380 | void TransferJob::setModificationTime(const QDateTime &mtime) |
381 | { |
382 | addMetaData(QStringLiteral("modified" ), value: mtime.toString(format: Qt::ISODate)); |
383 | } |
384 | |
385 | TransferJob *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 | |