1/*
2 SPDX-FileCopyrightText: 2000-2003 Waldo Bastian <bastian@kde.org>
3 SPDX-FileCopyrightText: 2000-2002 George Staikos <staikos@kde.org>
4 SPDX-FileCopyrightText: 2000-2002 Dawit Alemayehu <adawit@kde.org>
5 SPDX-FileCopyrightText: 2001, 2002 Hamish Rodda <rodda@kde.org>
6 SPDX-FileCopyrightText: 2007 Nick Shaforostoff <shafff@ukr.net>
7 SPDX-FileCopyrightText: 2007-2018 Daniel Nicoletti <dantti12@gmail.com>
8 SPDX-FileCopyrightText: 2008, 2009 Andreas Hartmetz <ahartmetz@gmail.com>
9 SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
10 SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
11
12 SPDX-License-Identifier: LGPL-2.0-or-later
13*/
14
15#include "http.h"
16#include "debug.h"
17#include "kioglobal_p.h"
18
19#include <QAuthenticator>
20#include <QBuffer>
21#include <QCoreApplication>
22#include <QDomDocument>
23#include <QMimeDatabase>
24#include <QNetworkAccessManager>
25#include <QNetworkCookie>
26#include <QNetworkCookieJar>
27#include <QNetworkProxy>
28#include <QNetworkReply>
29#include <QSslCipher>
30
31#include <KLocalizedString>
32
33#include <authinfo.h>
34#include <ksslcertificatemanager.h>
35
36#ifndef Q_OS_WIN
37#include <sys/utsname.h>
38#endif
39
40// Pseudo plugin class to embed meta data
41class KIOPluginForMetaData : public QObject
42{
43 Q_OBJECT
44 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.http" FILE "http.json")
45};
46
47extern "C" {
48int Q_DECL_EXPORT kdemain(int argc, char **argv)
49{
50 QCoreApplication app(argc, argv);
51 app.setApplicationName(QStringLiteral("kio_http"));
52
53 // start the worker
54 HTTPProtocol worker(argv[1], argv[2], argv[3]);
55 worker.dispatchLoop();
56 return 0;
57}
58}
59
60class Cookies : public QNetworkCookieJar
61{
62 Q_OBJECT
63public:
64 Q_SIGNAL void cookiesAdded(const QString &cookieString);
65 Q_SIGNAL void queryCookies(QString &cookieString);
66
67 QList<QNetworkCookie> m_cookies;
68
69 QList<QNetworkCookie> cookiesForUrl(const QUrl & /*url*/) const override
70 {
71 return m_cookies;
72 }
73
74 bool setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl & /*url*/) override
75 {
76 QString cookieString;
77
78 for (const QNetworkCookie &cookie : cookieList) {
79 cookieString += QStringLiteral("Set-Cookie: ") + QString::fromUtf8(ba: cookie.toRawForm()) + QLatin1Char('\n');
80 }
81
82 Q_EMIT cookiesAdded(cookieString);
83
84 return true;
85 }
86
87 void setCookies(const QString &cookieString)
88 {
89 const QStringList cookiePieces = cookieString.mid(position: 8).split(sep: QLatin1Char(';'), behavior: Qt::SkipEmptyParts);
90
91 for (const QString &cookiePiece : cookiePieces) {
92 const QString name = cookiePiece.left(n: cookiePiece.indexOf(ch: QLatin1Char('=')));
93 const QString value = cookiePiece.mid(position: cookiePiece.indexOf(ch: QLatin1Char('=')) + 1);
94
95 QNetworkCookie cookie(name.toUtf8(), value.toUtf8());
96 m_cookies << cookie;
97 }
98 }
99};
100
101namespace
102{
103
104bool isDav(const QString &protocol)
105{
106 return protocol.startsWith(s: QLatin1String("webdav")) || protocol.startsWith(s: QLatin1String("dav"));
107}
108
109QUrl protocolChangedToHttp(const QUrl &url)
110{
111 QUrl newUrl{url};
112 QString protocol{newUrl.scheme()};
113 // Keeps the 's' at the end, if present.
114 protocol.replace(before: QLatin1String{"webdav"}, after: QLatin1String{"http"});
115 protocol.replace(before: QLatin1String{"dav"}, after: QLatin1String{"http"});
116 if (newUrl.scheme() != protocol)
117 newUrl.setScheme(protocol);
118 return newUrl;
119}
120};
121
122HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app)
123 : WorkerBase(protocol, pool, app)
124{
125}
126
127HTTPProtocol::~HTTPProtocol()
128{
129}
130
131QString readMimeType(QNetworkReply *reply)
132{
133 const QString contentType = reply->header(header: QNetworkRequest::ContentTypeHeader).toString();
134
135 return contentType.left(n: contentType.indexOf(ch: QLatin1Char(';')));
136}
137
138void HTTPProtocol::handleSslErrors(QNetworkReply *reply, const QList<QSslError> errors)
139{
140 if (!metaData(QStringLiteral("ssl_no_ui")).isEmpty() && metaData(QStringLiteral("ssl_no_ui")).compare(other: QLatin1String("false"), cs: Qt::CaseInsensitive)) {
141 return;
142 }
143
144 QList<QSslCertificate> certs = reply->sslConfiguration().peerCertificateChain();
145
146 QStringList peerCertChain;
147 for (const QSslCertificate &cert : certs) {
148 peerCertChain += QString::fromUtf8(ba: cert.toPem());
149 }
150
151 auto sslErrors = errors;
152
153 const QList<QSslError> fatalErrors = KSslCertificateManager::nonIgnorableErrors(errors: sslErrors);
154 if (!fatalErrors.isEmpty()) {
155 qCWarning(KIOHTTP_LOG) << "SSL errors that cannot be ignored occured" << fatalErrors;
156 Q_EMIT errorOut(error: KIO::ERR_CANNOT_CONNECT);
157 return;
158 }
159
160 KSslCertificateRule rule = KSslCertificateManager::self()->rule(cert: certs.first(), hostName: m_hostName);
161
162 // remove previously seen and acknowledged errors
163 const QList<QSslError> remainingErrors = rule.filterErrors(errors: sslErrors);
164 if (remainingErrors.isEmpty()) {
165 reply->ignoreSslErrors();
166 return;
167 }
168
169 // try to fill in the blanks, i.e. missing certificates, and just assume that
170 // those belong to the peer (==website or similar) certificate.
171 for (int i = 0; i < sslErrors.count(); i++) {
172 if (sslErrors[i].certificate().isNull()) {
173 sslErrors[i] = QSslError(sslErrors[i].error(), certs[0]);
174 }
175 }
176
177 QStringList certificateErrors;
178 // encode the two-dimensional numeric error list using '\n' and '\t' as outer and inner separators
179 for (const QSslCertificate &cert : certs) {
180 QString errorStr;
181 for (const QSslError &error : std::as_const(t&: sslErrors)) {
182 if (error.certificate() == cert) {
183 errorStr = QString::number(static_cast<int>(error.error())) + QLatin1Char('\t');
184 }
185 }
186 if (errorStr.endsWith(c: QLatin1Char('\t'))) {
187 errorStr.chop(n: 1);
188 }
189 certificateErrors << errorStr;
190 }
191
192 const QSslCipher cipher = reply->sslConfiguration().sessionCipher();
193
194 const QVariantMap sslData{
195 {QStringLiteral("hostname"), m_hostName},
196 {QStringLiteral("protocol"), cipher.protocolString()},
197 {QStringLiteral("sslError"), errors.first().errorString()},
198 {QStringLiteral("peerCertChain"), peerCertChain},
199 {QStringLiteral("certificateErrors"), certificateErrors},
200 {QStringLiteral("cipher"), cipher.name()},
201 {QStringLiteral("bits"), cipher.supportedBits()},
202 {QStringLiteral("usedBits"), cipher.usedBits()},
203 };
204
205 int result = sslError(sslData);
206
207 if (result == 1) {
208 QDateTime ruleExpiry = QDateTime::currentDateTime();
209
210 const int result = messageBox(type: WarningTwoActionsCancel,
211 i18n("Would you like to accept this "
212 "certificate forever without "
213 "being prompted?"),
214 i18n("Server Authentication"),
215 i18n("&Forever"),
216 i18n("&Current Session only"));
217 if (result == WorkerBase::PrimaryAction) {
218 // accept forever ("for a very long time")
219 ruleExpiry = ruleExpiry.addYears(years: 1000);
220 } else if (result == WorkerBase::SecondaryAction) {
221 // accept "for a short time", half an hour.
222 ruleExpiry = ruleExpiry.addSecs(secs: 30 * 60);
223 } else {
224 Q_EMIT errorOut(error: KIO::ERR_CANNOT_CONNECT);
225 return;
226 }
227
228 rule.setExpiryDateTime(ruleExpiry);
229 rule.setIgnoredErrors(sslErrors);
230 KSslCertificateManager::self()->setRule(rule);
231
232 reply->ignoreSslErrors();
233 } else {
234 Q_EMIT errorOut(error: KIO::ERR_CANNOT_CONNECT);
235 }
236}
237
238HTTPProtocol::Response HTTPProtocol::makeDavRequest(const QUrl &url,
239 KIO::HTTP_METHOD method,
240 QByteArray &inputData,
241 DataMode dataMode,
242 const QMap<QByteArray, QByteArray> &extraHeaders)
243{
244 auto headers = extraHeaders;
245 const QString locks = davProcessLocks();
246
247 if (!headers.contains(key: "Content-Type")) {
248 headers.insert(key: "Content-Type", value: "text/xml; charset=utf-8");
249 }
250
251 if (!locks.isEmpty()) {
252 headers.insert(key: "If", value: locks.toLatin1());
253 }
254
255 return makeRequest(url, method, inputData, dataMode, extraHeaders: headers);
256}
257
258HTTPProtocol::Response
259HTTPProtocol::makeRequest(const QUrl &url, KIO::HTTP_METHOD method, QByteArray &inputData, DataMode dataMode, const QMap<QByteArray, QByteArray> &extraHeaders)
260{
261 /* HTTPProtocol::get(...) creates an empty inputData whether or not the calling function
262 * sent data to the request. QNetworkRequest sends "Content-Length: 0" for all requests
263 * when the device != nullptr, even if data is empty. Per RFC9110, "A user agent SHOULD NOT
264 * send a Content-Length header field when the request message does not contain content and
265 * the method semantics do not anticipate such data." Semantically, HTTP_GET, HTTP_HEAD,
266 * and others shouldn't send the header when the data is empty. Workaround that behavior
267 * here until and if Qt is modified. https://bugreports.qt.io/browse/QTBUG-138848 */
268 const bool noBodyWhenEmpty = (method == KIO::HTTP_GET || method == KIO::HTTP_HEAD || method == KIO::HTTP_DELETE);
269 QBuffer buffer(&inputData);
270 QIODevice *bodyDevice = (noBodyWhenEmpty && inputData.isEmpty()) ? nullptr : &buffer;
271 return makeRequest(url, method, inputData: bodyDevice, dataMode, extraHeaders);
272}
273
274static QString protocolForProxyType(QNetworkProxy::ProxyType type)
275{
276 switch (type) {
277 case QNetworkProxy::DefaultProxy:
278 break;
279 case QNetworkProxy::Socks5Proxy:
280 return QStringLiteral("socks");
281 case QNetworkProxy::NoProxy:
282 break;
283 case QNetworkProxy::HttpProxy:
284 case QNetworkProxy::HttpCachingProxy:
285 case QNetworkProxy::FtpCachingProxy:
286 break;
287 }
288
289 return QStringLiteral("http");
290}
291
292HTTPProtocol::Response HTTPProtocol::makeRequest(const QUrl &url,
293 KIO::HTTP_METHOD method,
294 QIODevice *inputData,
295 HTTPProtocol::DataMode dataMode,
296 const QMap<QByteArray, QByteArray> &extraHeaders)
297{
298 QNetworkAccessManager nam;
299
300 // Disable automatic redirect handling from Qt. We need to intercept redirects
301 // to let KIO handle them
302 nam.setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
303
304 auto cookies = new Cookies;
305
306 if (metaData(QStringLiteral("cookies")) == QStringLiteral("manual")) {
307 cookies->setCookies(metaData(QStringLiteral("setcookies")));
308
309 connect(sender: cookies, signal: &Cookies::cookiesAdded, context: this, slot: [this](const QString &cookiesString) {
310 setMetaData(QStringLiteral("setcookies"), value: cookiesString);
311 });
312 }
313
314 nam.setCookieJar(cookies);
315
316 QUrl properUrl = protocolChangedToHttp(url);
317
318 m_hostName = properUrl.host();
319
320 connect(sender: &nam, signal: &QNetworkAccessManager::authenticationRequired, context: this, slot: [this, url](QNetworkReply * /*reply*/, QAuthenticator *authenticator) {
321 if (configValue(QStringLiteral("no-www-auth"), defaultValue: false)) {
322 return;
323 }
324
325 KIO::AuthInfo authinfo;
326 authinfo.url = url;
327 authinfo.username = url.userName();
328 authinfo.prompt = i18n(
329 "You need to supply a username and a "
330 "password to access this site.");
331 authinfo.commentLabel = i18n("Site:");
332
333 // try to get credentials from kpasswdserver's cache, then try asking the user.
334 authinfo.verifyPath = false; // we have realm, no path based checking please!
335 authinfo.realmValue = authenticator->realm();
336
337 // Save the current authinfo url because it can be modified by the call to
338 // checkCachedAuthentication. That way we can restore it if the call
339 // modified it.
340 const QUrl reqUrl = authinfo.url;
341
342 if (checkCachedAuthentication(info&: authinfo)) {
343 authenticator->setUser(authinfo.username);
344 authenticator->setPassword(authinfo.password);
345 } else {
346 // Reset url to the saved url...
347 authinfo.url = reqUrl;
348 authinfo.keepPassword = true;
349 authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host());
350
351 const int errorCode = openPasswordDialog(info&: authinfo, errorMsg: QString());
352
353 if (!errorCode) {
354 authenticator->setUser(authinfo.username);
355 authenticator->setPassword(authinfo.password);
356 if (authinfo.keepPassword) {
357 cacheAuthentication(info: authinfo);
358 }
359 }
360 }
361 });
362
363 connect(sender: &nam, signal: &QNetworkAccessManager::proxyAuthenticationRequired, context: this, slot: [this](const QNetworkProxy &proxy, QAuthenticator *authenticator) {
364 if (configValue(QStringLiteral("no-proxy-auth"), defaultValue: false)) {
365 return;
366 }
367
368 QUrl proxyUrl;
369
370 proxyUrl.setScheme(protocolForProxyType(type: proxy.type()));
371 proxyUrl.setUserName(userName: proxy.user());
372 proxyUrl.setHost(host: proxy.hostName());
373 proxyUrl.setPort(proxy.port());
374
375 KIO::AuthInfo authinfo;
376 authinfo.url = proxyUrl;
377 authinfo.username = proxyUrl.userName();
378 authinfo.prompt = i18n(
379 "You need to supply a username and a password for "
380 "the proxy server listed below before you are allowed "
381 "to access any sites.");
382 authinfo.keepPassword = true;
383 authinfo.commentLabel = i18n("Proxy:");
384
385 // try to get credentials from kpasswdserver's cache, then try asking the user.
386 authinfo.verifyPath = false; // we have realm, no path based checking please!
387 authinfo.realmValue = authenticator->realm();
388 authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), proxyUrl.host());
389
390 // Save the current authinfo url because it can be modified by the call to
391 // checkCachedAuthentication. That way we can restore it if the call
392 // modified it.
393 const QUrl reqUrl = authinfo.url;
394
395 if (checkCachedAuthentication(info&: authinfo)) {
396 authenticator->setUser(authinfo.username);
397 authenticator->setPassword(authinfo.password);
398 } else {
399 // Reset url to the saved url...
400 authinfo.url = reqUrl;
401 authinfo.keepPassword = true;
402 authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host());
403
404 const int errorCode = openPasswordDialog(info&: authinfo, errorMsg: QString());
405
406 if (!errorCode) {
407 authenticator->setUser(authinfo.username);
408 authenticator->setPassword(authinfo.password);
409 if (authinfo.keepPassword) {
410 cacheAuthentication(info: authinfo);
411 }
412 }
413 }
414 });
415
416 QNetworkRequest request(properUrl);
417
418 const QByteArray contentType = getContentType().toUtf8();
419
420 if (!contentType.isEmpty()) {
421 request.setHeader(header: QNetworkRequest::ContentTypeHeader, value: contentType);
422 }
423
424 const QString referrer = metaData(QStringLiteral("referrer"));
425 if (!referrer.isEmpty()) {
426 request.setRawHeader(headerName: "Referer" /* sic! */, value: referrer.toUtf8());
427 }
428
429 QString userAgent = metaData(QStringLiteral("UserAgent"));
430 if (userAgent.isEmpty()) {
431 userAgent = defaultUserAgent();
432 }
433 request.setHeader(header: QNetworkRequest::UserAgentHeader, value: userAgent.toUtf8());
434
435 const QString accept = metaData(QStringLiteral("accept"));
436 if (!accept.isEmpty()) {
437 request.setRawHeader(headerName: "Accept", value: accept.toUtf8());
438 }
439
440 if (metaData(QStringLiteral("HttpVersion")) == QStringLiteral("http1")) {
441 request.setAttribute(code: QNetworkRequest::Http2AllowedAttribute, value: false);
442 }
443
444 for (auto [key, value] : extraHeaders.asKeyValueRange()) {
445 request.setRawHeader(headerName: key, value);
446 }
447
448 const QString customHeaders = metaData(QStringLiteral("customHTTPHeader"));
449 if (!customHeaders.isEmpty()) {
450 const QStringList headers = customHeaders.split(sep: QLatin1String("\r\n"));
451
452 for (const QString &header : headers) {
453 const QStringList split = header.split(sep: QLatin1String(": "));
454 Q_ASSERT(split.size() == 2);
455
456 request.setRawHeader(headerName: split[0].toUtf8(), value: split[1].toUtf8());
457 }
458 }
459
460 if (inputData) {
461 inputData->startTransaction(); // To be able to restart after redirects.
462 }
463
464 QNetworkReply *reply = nam.sendCustomRequest(request, verb: methodToString(method), data: inputData);
465
466 bool mimeTypeEmitted = false;
467
468 QEventLoop loop;
469
470 QObject::connect(sender: reply, signal: &QNetworkReply::sslErrors, context: &loop, slot: [this, reply](const QList<QSslError> errors) {
471 handleSslErrors(reply, errors);
472 });
473
474 qint64 lastTotalSize = -1;
475
476 QObject::connect(sender: reply, signal: &QNetworkReply::downloadProgress, context: this, slot: [this, &lastTotalSize](qint64 received, qint64 total) {
477 if (total != lastTotalSize) {
478 lastTotalSize = total;
479 totalSize(bytes: total);
480 }
481
482 processedSize(bytes: received);
483 });
484
485 // From RFC 4918 5.2 Collection Resources:
486 // > In general, clients SHOULD use the trailing slash form of collection names.
487 // > If clients do not use the trailing slash form the client needs to be prepared to see a redirect response.
488 // KIO doesn't handle trailing slashes well (especially in KDirLister), so handle it transparently.
489 bool redirectToTrailingSlash = false;
490
491 QObject::connect(sender: reply, signal: &QNetworkReply::metaDataChanged, slot: [this, &mimeTypeEmitted, &redirectToTrailingSlash, reply, dataMode, url, method]() {
492 const int statusCode = reply->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt();
493
494 if (statusCode >= 300 && statusCode < 400) {
495 const QString redir = reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute).toString();
496 const QUrl newUrl = url.resolved(relative: QUrl(redir));
497
498 // Handled after returning from the event loop.
499 // 301 is necessary for Apache (see bugs 209508 and 187970).
500 if (statusCode == 301 || statusCode == 307 || statusCode == 308) {
501 if (url != newUrl && url == newUrl.adjusted(options: QUrl::StripTrailingSlash)) {
502 redirectToTrailingSlash = true;
503 return;
504 }
505 }
506
507 if (statusCode == 301 || statusCode == 308) {
508 setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true"));
509 redirection(url: newUrl);
510 } else if (statusCode == 302) {
511 if (method == KIO::HTTP_POST) {
512 setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true"));
513 }
514
515 redirection(url: newUrl);
516 } else if (statusCode == 303) {
517 if (method != KIO::HTTP_HEAD) {
518 setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true"));
519 }
520
521 redirection(url: newUrl);
522 } else if (statusCode == 307) {
523 redirection(url: newUrl);
524 }
525 } else if (statusCode == 206) {
526 canResume();
527 }
528
529 if (!mimeTypeEmitted) {
530 mimeType(type: readMimeType(reply));
531 mimeTypeEmitted = true;
532 }
533
534 if (dataMode == Emit) {
535 // Limit how much data we fetch at a time to avoid storing it all in RAM
536 // do it in metaDataChanged to work around https://bugreports.qt.io/browse/QTBUG-15065
537 reply->setReadBufferSize(2048);
538 }
539 });
540
541 if (dataMode == Emit) {
542 QObject::connect(sender: reply, signal: &QNetworkReply::readyRead, context: &nam, slot: [this, reply] {
543 while (reply->bytesAvailable() > 0) {
544 QByteArray buf(2048, Qt::Uninitialized);
545 qint64 readBytes = reply->read(data: buf.data(), maxlen: 2048);
546 if (readBytes == 0) {
547 // End of data => don't emit the final data() call yet, the reply metadata is not yet complete!
548 break;
549 }
550 buf.truncate(pos: readBytes);
551 data(data: buf);
552 }
553 });
554 }
555
556 QObject::connect(sender: reply, signal: &QNetworkReply::finished, context: &loop, slot: &QEventLoop::quit);
557 QObject::connect(sender: this, signal: &HTTPProtocol::errorOut, context: &loop, slot: [this, &loop](KIO::Error error) {
558 lastError = error;
559 loop.quit();
560 });
561 loop.exec();
562
563 // If there was a foo -> foo/ redirect, follow it.
564 if (redirectToTrailingSlash) {
565 if (inputData) {
566 inputData->rollbackTransaction();
567 }
568 QUrl newUrl = url;
569 newUrl.setPath(path: newUrl.path() + QLatin1Char('/'));
570 return makeRequest(url: newUrl, method, inputData, dataMode, extraHeaders);
571 }
572 if (inputData) {
573 inputData->commitTransaction();
574 }
575
576 // make sure data is emitted at least once
577 // NOTE: emitting an empty data set means "end of data" and must not happen
578 // before we have set up our metadata properties etc. Only emit this at the
579 // very end of the function if applicable.
580 auto emitDataOnce = qScopeGuard(f: [this] {
581 data(data: QByteArray());
582 });
583
584 if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
585 reply->deleteLater();
586 return {.httpCode: 0, .data: QByteArray(), .kioCode: KIO::ERR_ACCESS_DENIED};
587 }
588
589 if (configValue(QStringLiteral("PropagateHttpHeader"), defaultValue: false)) {
590 QStringList headers;
591
592 const auto headerPairs = reply->rawHeaderPairs();
593 for (auto [key, value] : headerPairs) {
594 headers << QString::fromLatin1(ba: key + ": " + value);
595 }
596
597 setMetaData(QStringLiteral("HTTP-Headers"), value: headers.join(sep: QLatin1Char('\n')));
598 }
599
600 QByteArray returnData;
601
602 if (dataMode == Return) {
603 returnData = reply->readAll();
604 }
605
606 int statusCode = reply->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt();
607
608 setMetaData(QStringLiteral("responsecode"), value: QString::number(statusCode));
609 setMetaData(QStringLiteral("content-type"), value: readMimeType(reply));
610
611 reply->deleteLater();
612
613 return {.httpCode: statusCode, .data: returnData};
614}
615
616KIO::WorkerResult HTTPProtocol::get(const QUrl &url)
617{
618 QByteArray inputData = getData();
619
620 QString start = metaData(QStringLiteral("range-start"));
621
622 if (start.isEmpty()) {
623 // old name
624 start = metaData(QStringLiteral("resume"));
625 }
626
627 QMap<QByteArray, QByteArray> headers;
628
629 if (!start.isEmpty()) {
630 headers.insert(key: "Range", value: "bytes=" + start.toUtf8() + "-");
631 }
632
633 Response response = makeRequest(url, method: KIO::HTTP_GET, inputData, dataMode: DataMode::Emit, extraHeaders: headers);
634
635 return sendHttpError(url, method: KIO::HTTP_GET, response);
636}
637
638KIO::WorkerResult HTTPProtocol::put(const QUrl &url, int /*_mode*/, KIO::JobFlags flags)
639{
640 if (isDav(protocol: url.scheme())) {
641 if (!(flags & KIO::Overwrite)) {
642 // Checks if the destination exists and return an error if it does.
643 if (davDestinationExists(url)) {
644 return KIO::WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: url.fileName());
645 }
646 }
647 }
648
649 QByteArray inputData = getData();
650 Response response = makeRequest(url, method: KIO::HTTP_PUT, inputData, dataMode: DataMode::Emit);
651
652 return sendHttpError(url, method: KIO::HTTP_PUT, response);
653}
654
655KIO::WorkerResult HTTPProtocol::mimetype(const QUrl &url)
656{
657 QByteArray inputData = getData();
658 Response response = makeRequest(url, method: KIO::HTTP_HEAD, inputData, dataMode: DataMode::Discard);
659
660 return sendHttpError(url, method: KIO::HTTP_HEAD, response);
661}
662
663KIO::WorkerResult HTTPProtocol::post(const QUrl &url, qint64 /*size*/)
664{
665 QByteArray inputData = getData();
666 Response response = makeRequest(url, method: KIO::HTTP_POST, inputData, dataMode: DataMode::Emit);
667
668 return sendHttpError(url, method: KIO::HTTP_POST, response);
669}
670
671KIO::WorkerResult HTTPProtocol::special(const QByteArray &data)
672{
673 int tmp;
674 QDataStream stream(data);
675
676 stream >> tmp;
677 switch (tmp) {
678 case 1: { // HTTP POST
679 QUrl url;
680 qint64 size;
681 stream >> url >> size;
682 return post(url, size);
683 }
684 case 7: { // Generic WebDAV
685 QUrl url;
686 int method;
687 qint64 size;
688 stream >> url >> method >> size;
689 return davGeneric(url, method: (KIO::HTTP_METHOD)method, size);
690 }
691 }
692 return KIO::WorkerResult::pass();
693}
694
695QByteArray HTTPProtocol::getData()
696{
697 // TODO this is probably not great. Instead create a QIODevice that calls readData and pass that to QNAM?
698 QByteArray dataBuffer;
699
700 while (true) {
701 dataReq();
702
703 QByteArray buffer;
704 const int bytesRead = readData(buffer);
705
706 dataBuffer += buffer;
707
708 // On done...
709 if (bytesRead == 0) {
710 // sendOk = (bytesSent == m_iPostDataSize);
711 break;
712 }
713 }
714
715 return dataBuffer;
716}
717
718QString HTTPProtocol::getContentType()
719{
720 QString contentType = metaData(QStringLiteral("content-type"));
721 if (contentType.startsWith(s: QLatin1String("Content-Type: "), cs: Qt::CaseInsensitive)) {
722 contentType.remove(s: QLatin1String("Content-Type: "), cs: Qt::CaseInsensitive);
723 }
724 return contentType;
725}
726
727KIO::WorkerResult HTTPProtocol::listDir(const QUrl &url)
728{
729 return davStatList(url, stat: false);
730}
731
732KIO::WorkerResult HTTPProtocol::davStatList(const QUrl &url, bool stat)
733{
734 KIO::UDSEntry entry;
735
736 QMimeDatabase db;
737
738 KIO::HTTP_METHOD method;
739 QByteArray inputData;
740
741 // Maybe it's a disguised SEARCH...
742 QString query = metaData(QStringLiteral("davSearchQuery"));
743 if (!query.isEmpty()) {
744 inputData =
745 "<?xml version=\"1.0\"?>\r\n"
746 "<D:searchrequest xmlns:D=\"DAV:\">\r\n"
747 + query.toUtf8() + "</D:searchrequest>\r\n";
748
749 method = KIO::DAV_SEARCH;
750 } else {
751 // We are only after certain features...
752 inputData =
753 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
754 "<D:propfind xmlns:D=\"DAV:\">"
755 "<D:prop>"
756 "<D:creationdate/>"
757 "<D:getcontentlength/>"
758 "<D:displayname/>"
759 "<D:source/>"
760 "<D:getcontentlanguage/>"
761 "<D:getcontenttype/>"
762 "<D:getlastmodified/>"
763 "<D:getetag/>"
764 "<D:supportedlock/>"
765 "<D:lockdiscovery/>"
766 "<D:resourcetype/>"
767 "<D:quota-available-bytes/>"
768 "<D:quota-used-bytes/>"
769 "</D:prop>"
770 "</D:propfind>";
771 method = KIO::DAV_PROPFIND;
772 }
773
774 const QMap<QByteArray, QByteArray> extraHeaders = {
775 {"Depth", stat ? "0" : "1"},
776 };
777
778 Response response = makeDavRequest(url, method, inputData, dataMode: DataMode::Return, extraHeaders);
779
780 // Has a redirection already been called? If so, we're done.
781 // if (m_isRedirection || m_kioError) {
782 // if (m_isRedirection) {
783 // return davFinished();
784 // }
785 // return WorkerResult::pass();
786 // }
787
788 QDomDocument multiResponse;
789 multiResponse.setContent(data: response.data, options: QDomDocument::ParseOption::UseNamespaceProcessing);
790
791 bool hasResponse = false;
792
793 for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) {
794 QDomElement thisResponse = n.toElement();
795 if (thisResponse.isNull()) {
796 continue;
797 }
798
799 hasResponse = true;
800
801 QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement();
802 if (!href.isNull()) {
803 entry.clear();
804
805 const QUrl thisURL(href.text()); // href.text() is a percent-encoded url.
806 if (thisURL.isValid()) {
807 const QUrl adjustedThisURL = thisURL.adjusted(options: QUrl::StripTrailingSlash);
808 const QUrl adjustedUrl = url.adjusted(options: QUrl::StripTrailingSlash);
809
810 // base dir of a listDir(): name should be "."
811 QString name;
812 if (!stat && adjustedThisURL.path() == adjustedUrl.path()) {
813 name = QLatin1Char('.');
814 } else {
815 name = adjustedThisURL.fileName();
816 }
817
818 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: name.isEmpty() ? href.text() : name);
819 }
820
821 QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat"));
822
823 davParsePropstats(propstats, entry);
824
825 // Since a lot of webdav servers seem not to send the content-type information
826 // for the requested directory listings, we attempt to guess the MIME type from
827 // the resource name so long as the resource is not a directory.
828 if (entry.stringValue(field: KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(field: KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) {
829 QMimeType mime = db.mimeTypeForFile(fileName: thisURL.path(), mode: QMimeDatabase::MatchExtension);
830 if (mime.isValid() && !mime.isDefault()) {
831 // qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed MIME type for" << thisURL.path();
832 entry.fastInsert(field: KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, value: mime.name());
833 }
834 }
835
836 if (stat) {
837 // return an item
838 statEntry(entry: entry);
839 return KIO::WorkerResult::pass();
840 }
841 listEntry(entry);
842 } else {
843 // qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url;
844 }
845 }
846
847 if (stat || !hasResponse) {
848 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: url.toDisplayString());
849 }
850
851 return KIO::WorkerResult::pass();
852}
853
854void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, KIO::UDSEntry &entry)
855{
856 QString mimeType;
857 bool foundExecutable = false;
858 bool isDirectory = false;
859 uint lockCount = 0;
860 uint supportedLockCount = 0;
861 qlonglong quotaUsed = -1;
862 qlonglong quotaAvailable = -1;
863
864 for (int i = 0; i < propstats.count(); i++) {
865 QDomElement propstat = propstats.item(index: i).toElement();
866
867 QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement();
868 if (status.isNull()) {
869 // error, no status code in this propstat
870 // qCDebug(KIO_HTTP) << "Error, no status code in this propstat";
871 return;
872 }
873
874 int code = codeFromResponse(response: status.text());
875
876 if (code != 200) {
877 // qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)";
878 continue;
879 }
880
881 QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement();
882 if (prop.isNull()) {
883 // qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat.";
884 return;
885 }
886
887 // TODO unnecessary?
888 if (hasMetaData(QStringLiteral("davRequestResponse"))) {
889 QDomDocument doc;
890 doc.appendChild(newChild: prop);
891 entry.replace(field: KIO::UDSEntry::UDS_XML_PROPERTIES, value: doc.toString());
892 }
893
894 for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) {
895 QDomElement property = n.toElement();
896 if (property.isNull()) {
897 continue;
898 }
899
900 if (property.namespaceURI() != QLatin1String("DAV:")) {
901 // break out - we're only interested in properties from the DAV namespace
902 continue;
903 }
904
905 if (property.tagName() == QLatin1String("creationdate")) {
906 // Resource creation date. Should be is ISO 8601 format.
907 auto datetime = parseDateTime(input: property.text(), type: property.attribute(QStringLiteral("dt")));
908 if (datetime.isValid()) {
909 entry.replace(field: KIO::UDSEntry::UDS_CREATION_TIME, l: datetime.toSecsSinceEpoch());
910 } else {
911 qWarning() << "Failed to parse creationdate" << property.text() << property.attribute(QStringLiteral("dt"));
912 }
913 } else if (property.tagName() == QLatin1String("getcontentlength")) {
914 // Content length (file size)
915 entry.replace(field: KIO::UDSEntry::UDS_SIZE, l: property.text().toULong());
916 } else if (property.tagName() == QLatin1String("displayname")) {
917 // Name suitable for presentation to the user
918 setMetaData(QStringLiteral("davDisplayName"), value: property.text());
919 } else if (property.tagName() == QLatin1String("source")) {
920 // Source template location
921 QDomElement source = property.namedItem(QStringLiteral("link")).toElement().namedItem(QStringLiteral("dst")).toElement();
922 if (!source.isNull()) {
923 setMetaData(QStringLiteral("davSource"), value: source.text());
924 }
925 } else if (property.tagName() == QLatin1String("getcontentlanguage")) {
926 // equiv. to Content-Language header on a GET
927 setMetaData(QStringLiteral("davContentLanguage"), value: property.text());
928 } else if (property.tagName() == QLatin1String("getcontenttype")) {
929 // Content type (MIME type)
930 // This may require adjustments for other server-side webdav implementations
931 // (tested with Apache + mod_dav 1.0.3)
932 if (property.text() == QLatin1String("httpd/unix-directory")) {
933 isDirectory = true;
934 } else if (property.text() != QLatin1String("application/octet-stream")) {
935 // The server could be lazy and always return application/octet-stream;
936 // we will guess the MIME type later in that case.
937 mimeType = property.text();
938 }
939 } else if (property.tagName() == QLatin1String("executable")) {
940 // File executable status
941 if (property.text() == QLatin1Char('T')) {
942 foundExecutable = true;
943 }
944
945 } else if (property.tagName() == QLatin1String("getlastmodified")) {
946 // Last modification date
947 auto datetime = parseDateTime(input: property.text(), type: property.attribute(QStringLiteral("dt")));
948 if (datetime.isValid()) {
949 entry.replace(field: KIO::UDSEntry::UDS_MODIFICATION_TIME, l: datetime.toSecsSinceEpoch());
950 } else {
951 qWarning() << "Failed to parse getlastmodified" << property.text() << property.attribute(QStringLiteral("dt"));
952 }
953 } else if (property.tagName() == QLatin1String("getetag")) {
954 // Entity tag
955 setMetaData(QStringLiteral("davEntityTag"), value: property.text());
956 } else if (property.tagName() == QLatin1String("supportedlock")) {
957 // Supported locking specifications
958 for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) {
959 QDomElement lockEntry = n2.toElement();
960 if (lockEntry.tagName() == QLatin1String("lockentry")) {
961 QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement();
962 QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement();
963 if (!lockScope.isNull() && !lockType.isNull()) {
964 // Lock type was properly specified
965 supportedLockCount++;
966 const QString lockCountStr = QString::number(supportedLockCount);
967 const QString scope = lockScope.firstChild().toElement().tagName();
968 const QString type = lockType.firstChild().toElement().tagName();
969
970 setMetaData(key: QLatin1String("davSupportedLockScope") + lockCountStr, value: scope);
971 setMetaData(key: QLatin1String("davSupportedLockType") + lockCountStr, value: type);
972 }
973 }
974 }
975 } else if (property.tagName() == QLatin1String("lockdiscovery")) {
976 // Lists the available locks
977 davParseActiveLocks(activeLocks: property.elementsByTagName(QStringLiteral("activelock")), lockCount);
978 } else if (property.tagName() == QLatin1String("resourcetype")) {
979 // Resource type. "Specifies the nature of the resource."
980 if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) {
981 // This is a collection (directory)
982 isDirectory = true;
983 }
984 } else if (property.tagName() == QLatin1String("quota-used-bytes")) {
985 // Quota-used-bytes. "Contains the amount of storage already in use."
986 bool ok;
987 qlonglong used = property.text().toLongLong(ok: &ok);
988 if (ok) {
989 quotaUsed = used;
990 }
991 } else if (property.tagName() == QLatin1String("quota-available-bytes")) {
992 // Quota-available-bytes. "Indicates the maximum amount of additional storage available."
993 bool ok;
994 qlonglong available = property.text().toLongLong(ok: &ok);
995 if (ok) {
996 quotaAvailable = available;
997 }
998 } else {
999 // qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName();
1000 }
1001 }
1002 }
1003
1004 setMetaData(QStringLiteral("davLockCount"), value: QString::number(lockCount));
1005 setMetaData(QStringLiteral("davSupportedLockCount"), value: QString::number(supportedLockCount));
1006
1007 entry.replace(field: KIO::UDSEntry::UDS_FILE_TYPE, l: isDirectory ? S_IFDIR : S_IFREG);
1008
1009 if (foundExecutable || isDirectory) {
1010 // File was executable, or is a directory.
1011 entry.replace(field: KIO::UDSEntry::UDS_ACCESS, l: 0700);
1012 } else {
1013 entry.replace(field: KIO::UDSEntry::UDS_ACCESS, l: 0600);
1014 }
1015
1016 if (!isDirectory && !mimeType.isEmpty()) {
1017 entry.replace(field: KIO::UDSEntry::UDS_MIME_TYPE, value: mimeType);
1018 }
1019
1020 if (quotaUsed >= 0 && quotaAvailable >= 0) {
1021 // Only used and available storage properties exist, the total storage size has to be calculated.
1022 setMetaData(QStringLiteral("total"), value: QString::number(quotaUsed + quotaAvailable));
1023 setMetaData(QStringLiteral("available"), value: QString::number(quotaAvailable));
1024 }
1025}
1026
1027void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount)
1028{
1029 for (int i = 0; i < activeLocks.count(); i++) {
1030 const QDomElement activeLock = activeLocks.item(index: i).toElement();
1031
1032 lockCount++;
1033 // required
1034 const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement();
1035 const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement();
1036 const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement();
1037 // optional
1038 const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement();
1039 const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement();
1040 const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement();
1041
1042 if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) {
1043 // lock was properly specified
1044 lockCount++;
1045 const QString lockCountStr = QString::number(lockCount);
1046 const QString scope = lockScope.firstChild().toElement().tagName();
1047 const QString type = lockType.firstChild().toElement().tagName();
1048 const QString depth = lockDepth.text();
1049
1050 setMetaData(key: QLatin1String("davLockScope") + lockCountStr, value: scope);
1051 setMetaData(key: QLatin1String("davLockType") + lockCountStr, value: type);
1052 setMetaData(key: QLatin1String("davLockDepth") + lockCountStr, value: depth);
1053
1054 if (!lockOwner.isNull()) {
1055 setMetaData(key: QLatin1String("davLockOwner") + lockCountStr, value: lockOwner.text());
1056 }
1057
1058 if (!lockTimeout.isNull()) {
1059 setMetaData(key: QLatin1String("davLockTimeout") + lockCountStr, value: lockTimeout.text());
1060 }
1061
1062 if (!lockToken.isNull()) {
1063 QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement();
1064 if (!tokenVal.isNull()) {
1065 setMetaData(key: QLatin1String("davLockToken") + lockCountStr, value: tokenVal.text());
1066 }
1067 }
1068 }
1069 }
1070}
1071
1072QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type)
1073{
1074 if (type == QLatin1String("dateTime.tz")) {
1075 return QDateTime::fromString(string: input, format: Qt::ISODate);
1076 }
1077
1078 // Qt decided to no longer support "GMT" for some reason: QTBUG-114681
1079 QString inputUtc = input;
1080 inputUtc.replace(before: QLatin1String("GMT"), after: QLatin1String("+0000"));
1081
1082 if (type == QLatin1String("dateTime.rfc1123")) {
1083 return QDateTime::fromString(string: inputUtc, format: Qt::RFC2822Date);
1084 }
1085
1086 // format not advertised... try to parse anyway
1087 QDateTime time = QDateTime::fromString(string: inputUtc, format: Qt::RFC2822Date);
1088 if (time.isValid()) {
1089 return time;
1090 }
1091
1092 return QDateTime::fromString(string: input, format: Qt::ISODate);
1093}
1094
1095int HTTPProtocol::codeFromResponse(const QString &response)
1096{
1097 const int firstSpace = response.indexOf(ch: QLatin1Char(' '));
1098 const int secondSpace = response.indexOf(ch: QLatin1Char(' '), from: firstSpace + 1);
1099
1100 return QStringView(response).mid(pos: firstSpace + 1, n: secondSpace - firstSpace - 1).toInt();
1101}
1102
1103KIO::WorkerResult HTTPProtocol::stat(const QUrl &url)
1104{
1105 if (!isDav(protocol: url.scheme())) {
1106 QString statSide = metaData(QStringLiteral("statSide"));
1107 if (statSide != QLatin1String("source")) {
1108 // When uploading we assume the file does not exist.
1109 return KIO::WorkerResult::fail(error: KIO::ERR_DOES_NOT_EXIST, errorString: url.toDisplayString());
1110 }
1111
1112 // When downloading we assume it exists
1113 KIO::UDSEntry entry;
1114 entry.reserve(size: 3);
1115 entry.fastInsert(field: KIO::UDSEntry::UDS_NAME, value: url.fileName());
1116 entry.fastInsert(field: KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file
1117 entry.fastInsert(field: KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody
1118
1119 statEntry(entry: entry);
1120 return KIO::WorkerResult::pass();
1121 }
1122
1123 return davStatList(url, stat: true);
1124}
1125
1126KIO::WorkerResult HTTPProtocol::mkdir(const QUrl &url, int)
1127{
1128 QByteArray inputData;
1129 Response response = makeDavRequest(url, method: KIO::DAV_MKCOL, inputData, dataMode: DataMode::Discard);
1130
1131 if (response.httpCode != 201) {
1132 return davError(method: KIO::DAV_MKCOL, url, response);
1133 }
1134 return KIO::WorkerResult::pass();
1135}
1136
1137KIO::WorkerResult HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
1138{
1139 QMap<QByteArray, QByteArray> extraHeaders = {
1140 {"Destination", dest.toString(options: QUrl::FullyEncoded).toUtf8()},
1141 {"Overwrite", (flags & KIO::Overwrite) ? "T" : "F"},
1142 {"Depth", "infinity"},
1143 };
1144
1145 QByteArray inputData;
1146 Response response = makeDavRequest(url: src, method: KIO::DAV_MOVE, inputData, dataMode: DataMode::Discard, extraHeaders);
1147
1148 // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion
1149 if (response.httpCode == 201 || response.httpCode == 204) {
1150 return KIO::WorkerResult::pass();
1151 }
1152 return davError(method: KIO::DAV_MOVE, url: src, response);
1153}
1154
1155KIO::WorkerResult HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags)
1156{
1157 const bool isSourceLocal = src.isLocalFile();
1158 const bool isDestinationLocal = dest.isLocalFile();
1159
1160 if (isSourceLocal && !isDestinationLocal) {
1161 return copyPut(src, dest, flags);
1162 }
1163
1164 if (!(flags & KIO::Overwrite)) {
1165 // Checks if the destination exists and return an error if it does.
1166 if (davDestinationExists(url: dest)) {
1167 return KIO::WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: dest.fileName());
1168 }
1169 }
1170
1171 QMap<QByteArray, QByteArray> extraHeaders = {
1172 {"Destination", dest.toString(options: QUrl::FullyEncoded).toUtf8()},
1173 {"Overwrite", (flags & KIO::Overwrite) ? "T" : "F"},
1174 {"Depth", "infinity"},
1175 };
1176
1177 QByteArray inputData;
1178 Response response = makeDavRequest(url: src, method: KIO::DAV_COPY, inputData, dataMode: DataMode::Discard, extraHeaders);
1179
1180 // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion
1181 if (response.httpCode == 201 || response.httpCode == 204) {
1182 return KIO::WorkerResult::pass();
1183 }
1184
1185 return davError(method: KIO::DAV_COPY, url: src, response);
1186}
1187
1188KIO::WorkerResult HTTPProtocol::del(const QUrl &url, bool)
1189{
1190 if (isDav(protocol: url.scheme())) {
1191 Response response = makeRequest(url, method: KIO::HTTP_DELETE, inputData: {}, dataMode: DataMode::Discard);
1192
1193 // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content
1194 // on successful completion.
1195 if (response.httpCode == 200 || response.httpCode == 204 /*|| m_isRedirection*/) {
1196 return KIO::WorkerResult::pass();
1197 }
1198 return davError(method: KIO::HTTP_DELETE, url, response);
1199 }
1200
1201 Response response = makeRequest(url, method: KIO::HTTP_DELETE, inputData: {}, dataMode: DataMode::Discard);
1202
1203 return sendHttpError(url, method: KIO::HTTP_DELETE, response);
1204}
1205
1206KIO::WorkerResult HTTPProtocol::copyPut(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
1207{
1208 if (!(flags & KIO::Overwrite)) {
1209 // Checks if the destination exists and return an error if it does.
1210 if (davDestinationExists(url: dest)) {
1211 return KIO::WorkerResult::fail(error: KIO::ERR_FILE_ALREADY_EXIST, errorString: dest.fileName());
1212 }
1213 }
1214
1215 auto sourceFile = new QFile(src.toLocalFile());
1216 if (!sourceFile->open(flags: QFile::ReadOnly)) {
1217 return KIO::WorkerResult::fail(error: KIO::ERR_CANNOT_OPEN_FOR_READING, errorString: src.fileName());
1218 }
1219
1220 Response response = makeRequest(url: dest, method: KIO::HTTP_PUT, inputData: sourceFile, dataMode: {});
1221
1222 return sendHttpError(url: dest, method: KIO::HTTP_PUT, response);
1223}
1224
1225bool HTTPProtocol::davDestinationExists(const QUrl &url)
1226{
1227 QByteArray request(
1228 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
1229 "<D:propfind xmlns:D=\"DAV:\"><D:prop>"
1230 "<D:creationdate/>"
1231 "<D:getcontentlength/>"
1232 "<D:displayname/>"
1233 "<D:resourcetype/>"
1234 "</D:prop></D:propfind>");
1235
1236 const QMap<QByteArray, QByteArray> extraHeaders = {
1237 {"Depth", "0"},
1238 };
1239
1240 Response response = makeDavRequest(url, method: KIO::DAV_PROPFIND, inputData&: request, dataMode: DataMode::Discard, extraHeaders);
1241
1242 if (response.httpCode >= 200 && response.httpCode < 300) {
1243 // 2XX means the file exists. This includes 207 (multi-status response).
1244 // qCDebug(KIO_HTTP) << "davDestinationExists: file exists. code:" << m_request.responseCode;
1245 return true;
1246 } else {
1247 // qCDebug(KIO_HTTP) << "davDestinationExists: file does not exist. code:" << m_request.responseCode;
1248 }
1249
1250 return false;
1251}
1252
1253KIO::WorkerResult HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size)
1254{
1255 // TODO what about size?
1256 Q_UNUSED(size)
1257 QMap<QByteArray, QByteArray> extraHeaders;
1258
1259 if (method == KIO::DAV_PROPFIND || method == KIO::DAV_REPORT) {
1260 int depth = 0;
1261
1262 if (hasMetaData(QStringLiteral("davDepth"))) {
1263 depth = metaData(QStringLiteral("davDepth")).toInt();
1264 } else {
1265 // TODO is warning here appropriate?
1266 qWarning() << "Performing DAV PROPFIND or REPORT without specifying davDepth";
1267 }
1268
1269 extraHeaders.insert(key: "Depth", value: QByteArray::number(depth));
1270 }
1271
1272 QByteArray inputData = getData();
1273 Response response = makeDavRequest(url, method, inputData, dataMode: DataMode::Emit, extraHeaders);
1274
1275 // TODO old code seems to use http error, not dav error
1276 return sendHttpError(url, method, response);
1277}
1278
1279KIO::WorkerResult HTTPProtocol::fileSystemFreeSpace(const QUrl &url)
1280{
1281 return davStatList(url, stat: true);
1282}
1283
1284KIO::WorkerResult HTTPProtocol::davError(KIO::HTTP_METHOD method, const QUrl &url, const Response &response)
1285{
1286 if (response.kioCode == KIO::ERR_ACCESS_DENIED) {
1287 return KIO::WorkerResult::fail(error: response.kioCode, errorString: url.toDisplayString());
1288 }
1289
1290 QString discard;
1291 return davError(errorMsg&: discard, method, code: response.httpCode, url: url, responseData: response.data);
1292}
1293
1294QString HTTPProtocol::davProcessLocks()
1295{
1296 if (hasMetaData(QStringLiteral("davLockCount"))) {
1297 QString response;
1298 int numLocks = metaData(QStringLiteral("davLockCount")).toInt();
1299 bool bracketsOpen = false;
1300 for (int i = 0; i < numLocks; i++) {
1301 const QString countStr = QString::number(i);
1302 if (hasMetaData(key: QLatin1String("davLockToken") + countStr)) {
1303 if (hasMetaData(key: QLatin1String("davLockURL") + countStr)) {
1304 if (bracketsOpen) {
1305 response += QLatin1Char(')');
1306 bracketsOpen = false;
1307 }
1308 response += QLatin1String(" <") + metaData(key: QLatin1String("davLockURL") + countStr) + QLatin1Char('>');
1309 }
1310
1311 if (!bracketsOpen) {
1312 response += QLatin1String(" (");
1313 bracketsOpen = true;
1314 } else {
1315 response += QLatin1Char(' ');
1316 }
1317
1318 if (hasMetaData(key: QLatin1String("davLockNot") + countStr)) {
1319 response += QLatin1String("Not ");
1320 }
1321
1322 response += QLatin1Char('<') + metaData(key: QLatin1String("davLockToken") + countStr) + QLatin1Char('>');
1323 }
1324 }
1325
1326 if (bracketsOpen) {
1327 response += QLatin1Char(')');
1328 }
1329
1330 return response;
1331 }
1332
1333 return QString();
1334}
1335
1336KIO::WorkerResult HTTPProtocol::davError(QString &errorMsg, KIO::HTTP_METHOD method, int code, const QUrl &url, const QByteArray &responseData)
1337{
1338 QString action;
1339 QString errorString;
1340 int errorCode = KIO::ERR_WORKER_DEFINED;
1341
1342 // for 412 Precondition Failed
1343 QString ow = i18n("Otherwise, the request would have succeeded.");
1344
1345 if (method == KIO::DAV_PROPFIND) {
1346 action = i18nc("request type", "retrieve property values");
1347 } else if (method == KIO::DAV_PROPPATCH) {
1348 action = i18nc("request type", "set property values");
1349 } else if (method == KIO::DAV_MKCOL) {
1350 action = i18nc("request type", "create the requested folder");
1351 } else if (method == KIO::DAV_COPY) {
1352 action = i18nc("request type", "copy the specified file or folder");
1353 } else if (method == KIO::DAV_MOVE) {
1354 action = i18nc("request type", "move the specified file or folder");
1355 } else if (method == KIO::DAV_SEARCH) {
1356 action = i18nc("request type", "search in the specified folder");
1357 } else if (method == KIO::DAV_LOCK) {
1358 action = i18nc("request type", "lock the specified file or folder");
1359 } else if (method == KIO::DAV_UNLOCK) {
1360 action = i18nc("request type", "unlock the specified file or folder");
1361 } else if (method == KIO::HTTP_DELETE) {
1362 action = i18nc("request type", "delete the specified file or folder");
1363 } else if (method == KIO::HTTP_OPTIONS) {
1364 action = i18nc("request type", "query the server's capabilities");
1365 } else if (method == KIO::HTTP_GET) {
1366 action = i18nc("request type", "retrieve the contents of the specified file or folder");
1367 } else if (method == KIO::DAV_REPORT) {
1368 action = i18nc("request type", "run a report in the specified folder");
1369 } else {
1370 // this should not happen, this function is for webdav errors only
1371 Q_ASSERT(0);
1372 }
1373
1374 // default error message if the following code fails
1375 errorString = i18nc("%1: code, %2: request type",
1376 "An unexpected error (%1) occurred "
1377 "while attempting to %2.",
1378 code,
1379 action);
1380
1381 switch (code) {
1382 case 207:
1383 // 207 Multi-status
1384 {
1385 // our error info is in the returned XML document.
1386 // retrieve the XML document
1387
1388 QStringList errors;
1389 QDomDocument multiResponse;
1390 multiResponse.setContent(data: responseData, options: QDomDocument::ParseOption::UseNamespaceProcessing);
1391
1392 QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement();
1393
1394 QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response"));
1395
1396 for (int i = 0; i < responses.count(); i++) {
1397 int errCode;
1398 QUrl errUrl;
1399
1400 QDomElement response = responses.item(index: i).toElement();
1401 QDomElement code = response.namedItem(QStringLiteral("status")).toElement();
1402
1403 if (!code.isNull()) {
1404 errCode = codeFromResponse(response: code.text());
1405 QDomElement href = response.namedItem(QStringLiteral("href")).toElement();
1406 if (!href.isNull()) {
1407 errUrl = QUrl(href.text());
1408 }
1409 QString error;
1410 (void)davError(errorMsg&: error, method, code: errCode, url: errUrl, responseData: {});
1411 errors << error;
1412 }
1413 }
1414
1415 errorString = i18nc("%1: request type, %2: url",
1416 "An error occurred while attempting to %1, %2. A "
1417 "summary of the reasons is below.",
1418 action,
1419 url.toString());
1420
1421 errorString += QLatin1String("<ul>");
1422
1423 for (const QString &error : std::as_const(t&: errors)) {
1424 errorString += QLatin1String("<li>") + error + QLatin1String("</li>");
1425 }
1426
1427 errorString += QLatin1String("</ul>");
1428 }
1429 break;
1430 case 403:
1431 case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1432 // 403 Forbidden
1433 // ERR_ACCESS_DENIED
1434 errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action);
1435 break;
1436 case 405:
1437 // 405 Method Not Allowed
1438 if (method == KIO::DAV_MKCOL) {
1439 // ERR_DIR_ALREADY_EXIST
1440 errorString = url.toString();
1441 errorCode = KIO::ERR_DIR_ALREADY_EXIST;
1442 }
1443 break;
1444 case 409:
1445 // 409 Conflict
1446 // ERR_ACCESS_DENIED
1447 errorString = i18n(
1448 "A resource cannot be created at the destination "
1449 "until one or more intermediate collections (folders) "
1450 "have been created.");
1451 break;
1452 case 412:
1453 // 412 Precondition failed
1454 if (method == KIO::DAV_COPY || method == KIO::DAV_MOVE) {
1455 // ERR_ACCESS_DENIED
1456 errorString = i18n(
1457 "The server was unable to maintain the liveness of the\n"
1458 "properties listed in the propertybehavior XML element\n"
1459 "or you attempted to overwrite a file while requesting\n"
1460 "that files are not overwritten.\n %1",
1461 ow);
1462
1463 } else if (method == KIO::DAV_LOCK) {
1464 // ERR_ACCESS_DENIED
1465 errorString = i18n("The requested lock could not be granted. %1", ow);
1466 }
1467 break;
1468 case 415:
1469 // 415 Unsupported Media Type
1470 // ERR_ACCESS_DENIED
1471 errorString = i18n("The server does not support the request type of the body.");
1472 break;
1473 case 423:
1474 // 423 Locked
1475 // ERR_ACCESS_DENIED
1476 errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action);
1477 break;
1478 case 425:
1479 // 424 Failed Dependency
1480 errorString = i18n("This action was prevented by another error.");
1481 break;
1482 case 502:
1483 // 502 Bad Gateway
1484 if (method == KIO::DAV_COPY || method == KIO::DAV_MOVE) {
1485 // ERR_WRITE_ACCESS_DENIED
1486 errorString = i18nc("%1: request type",
1487 "Unable to %1 because the destination server refuses "
1488 "to accept the file or folder.",
1489 action);
1490 }
1491 break;
1492 case 507:
1493 // 507 Insufficient Storage
1494 // ERR_DISK_FULL
1495 errorString = i18n(
1496 "The destination resource does not have sufficient space "
1497 "to record the state of the resource after the execution "
1498 "of this method.");
1499 break;
1500 default:
1501 break;
1502 }
1503
1504 errorMsg = errorString;
1505 return KIO::WorkerResult::fail(error: errorCode, errorString: errorString);
1506}
1507
1508// HTTP generic error
1509static int httpGenericError(int responseCode, QString *errorString)
1510{
1511 Q_ASSERT(errorString);
1512
1513 int errorCode = 0;
1514 errorString->clear();
1515
1516 if (responseCode == 204) {
1517 errorCode = KIO::ERR_NO_CONTENT;
1518 }
1519
1520 if (responseCode >= 400 && responseCode <= 499) {
1521 errorCode = KIO::ERR_DOES_NOT_EXIST;
1522 }
1523
1524 if (responseCode >= 500 && responseCode <= 599) {
1525 errorCode = KIO::ERR_INTERNAL_SERVER;
1526 }
1527
1528 return errorCode;
1529}
1530
1531// HTTP DELETE specific errors
1532static int httpDelError(int responseCode, QString *errorString)
1533{
1534 Q_ASSERT(errorString);
1535
1536 int errorCode = 0;
1537 errorString->clear();
1538
1539 switch (responseCode) {
1540 case 204:
1541 errorCode = KIO::ERR_NO_CONTENT;
1542 break;
1543 default:
1544 break;
1545 }
1546
1547 if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) {
1548 errorCode = KIO::ERR_WORKER_DEFINED;
1549 *errorString = i18n("The resource cannot be deleted.");
1550 }
1551
1552 if (responseCode >= 400 && responseCode <= 499) {
1553 errorCode = KIO::ERR_DOES_NOT_EXIST;
1554 }
1555
1556 if (responseCode >= 500 && responseCode <= 599) {
1557 errorCode = KIO::ERR_INTERNAL_SERVER;
1558 }
1559
1560 return errorCode;
1561}
1562
1563// HTTP PUT specific errors
1564static int httpPutError(const QUrl &url, int responseCode, QString *errorString)
1565{
1566 Q_ASSERT(errorString);
1567
1568 int errorCode = 0;
1569 const QString action(i18nc("request type", "upload %1", url.toDisplayString()));
1570
1571 switch (responseCode) {
1572 case 403:
1573 case 405:
1574 case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1575 // 403 Forbidden
1576 // 405 Method Not Allowed
1577 // ERR_ACCESS_DENIED
1578 *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action);
1579 errorCode = KIO::ERR_WORKER_DEFINED;
1580 break;
1581 case 409:
1582 // 409 Conflict
1583 // ERR_ACCESS_DENIED
1584 *errorString = i18n(
1585 "A resource cannot be created at the destination "
1586 "until one or more intermediate collections (folders) "
1587 "have been created.");
1588 errorCode = KIO::ERR_WORKER_DEFINED;
1589 break;
1590 case 423:
1591 // 423 Locked
1592 // ERR_ACCESS_DENIED
1593 *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action);
1594 errorCode = KIO::ERR_WORKER_DEFINED;
1595 break;
1596 case 502:
1597 // 502 Bad Gateway
1598 // ERR_WRITE_ACCESS_DENIED;
1599 *errorString = i18nc("%1: request type",
1600 "Unable to %1 because the destination server refuses "
1601 "to accept the file or folder.",
1602 action);
1603 errorCode = KIO::ERR_WORKER_DEFINED;
1604 break;
1605 case 507:
1606 // 507 Insufficient Storage
1607 // ERR_DISK_FULL
1608 *errorString = i18n(
1609 "The destination resource does not have sufficient space "
1610 "to record the state of the resource after the execution "
1611 "of this method.");
1612 errorCode = KIO::ERR_WORKER_DEFINED;
1613 break;
1614 default:
1615 break;
1616 }
1617
1618 if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) {
1619 errorCode = KIO::ERR_WORKER_DEFINED;
1620 *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action);
1621 }
1622
1623 if (responseCode >= 400 && responseCode <= 499) {
1624 errorCode = KIO::ERR_DOES_NOT_EXIST;
1625 }
1626
1627 if (responseCode >= 500 && responseCode <= 599) {
1628 errorCode = KIO::ERR_INTERNAL_SERVER;
1629 }
1630
1631 return errorCode;
1632}
1633
1634KIO::WorkerResult HTTPProtocol::sendHttpError(const QUrl &url, KIO::HTTP_METHOD method, const HTTPProtocol::Response &response)
1635{
1636 QString errorString;
1637 int errorCode = 0;
1638
1639 if (response.kioCode == KIO::ERR_ACCESS_DENIED) {
1640 return KIO::WorkerResult::fail(error: response.kioCode, errorString: url.toDisplayString());
1641 }
1642
1643 int responseCode = response.httpCode;
1644
1645 if (method == KIO::HTTP_PUT) {
1646 errorCode = httpPutError(url, responseCode, errorString: &errorString);
1647 } else if (method == KIO::HTTP_DELETE) {
1648 errorCode = httpDelError(responseCode, errorString: &errorString);
1649 } else {
1650 errorCode = httpGenericError(responseCode, errorString: &errorString);
1651 }
1652
1653 if (errorCode) {
1654 if (errorCode == KIO::ERR_DOES_NOT_EXIST) {
1655 errorString = url.toDisplayString();
1656 }
1657
1658 return KIO::WorkerResult::fail(error: errorCode, errorString: errorString);
1659 }
1660
1661 return KIO::WorkerResult::pass();
1662}
1663
1664QByteArray HTTPProtocol::methodToString(KIO::HTTP_METHOD method)
1665{
1666 switch (method) {
1667 case KIO::HTTP_GET:
1668 return "GET";
1669 case KIO::HTTP_PUT:
1670 return "PUT";
1671 case KIO::HTTP_POST:
1672 return "POST";
1673 case KIO::HTTP_HEAD:
1674 return "HEAD";
1675 case KIO::HTTP_DELETE:
1676 return "DELETE";
1677 case KIO::HTTP_OPTIONS:
1678 return "OPTIONS";
1679 case KIO::DAV_PROPFIND:
1680 return "PROPFIND";
1681 case KIO::DAV_PROPPATCH:
1682 return "PROPPATCH";
1683 case KIO::DAV_MKCOL:
1684 return "MKCOL";
1685 case KIO::DAV_COPY:
1686 return "COPY";
1687 case KIO::DAV_MOVE:
1688 return "MOVE";
1689 case KIO::DAV_LOCK:
1690 return "LOCK";
1691 case KIO::DAV_UNLOCK:
1692 return "UNLOCK";
1693 case KIO::DAV_SEARCH:
1694 return "SEARCH";
1695 case KIO::DAV_SUBSCRIBE:
1696 return "SUBSCRIBE";
1697 case KIO::DAV_UNSUBSCRIBE:
1698 return "UNSUBSCRIBE";
1699 case KIO::DAV_POLL:
1700 return "POLL";
1701 case KIO::DAV_NOTIFY:
1702 return "NOTIFY";
1703 case KIO::DAV_REPORT:
1704 return "REPORT";
1705 default:
1706 Q_ASSERT(false);
1707 return QByteArray();
1708 }
1709}
1710
1711// This is not the OS, but the windowing system, e.g. X11 on Unix/Linux.
1712static QString platform()
1713{
1714#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
1715 return QStringLiteral("X11");
1716#elif defined(Q_OS_MAC)
1717 return QStringLiteral("Macintosh");
1718#elif defined(Q_OS_WIN)
1719 return QStringLiteral("Windows");
1720#else
1721 return QStringLiteral("Unknown");
1722#endif
1723}
1724
1725QString HTTPProtocol::defaultUserAgent()
1726{
1727 if (!m_defaultUserAgent.isEmpty()) {
1728 return m_defaultUserAgent;
1729 }
1730
1731 QString systemName;
1732 QString machine;
1733 QString supp;
1734 const bool sysInfoFound = getSystemNameVersionAndMachine(systemName, machine);
1735
1736 supp += platform();
1737
1738 if (sysInfoFound) {
1739 supp += QLatin1String("; ") + systemName;
1740 supp += QLatin1Char(' ') + machine;
1741 }
1742
1743 QString appName = QCoreApplication::applicationName();
1744 if (appName.isEmpty() || appName.startsWith(s: QLatin1String("kcmshell"), cs: Qt::CaseInsensitive)) {
1745 appName = QStringLiteral("KDE");
1746 }
1747 QString appVersion = QCoreApplication::applicationVersion();
1748 if (appVersion.isEmpty()) {
1749 appVersion += QLatin1String(KIO_VERSION_STRING);
1750 }
1751
1752 m_defaultUserAgent = QLatin1String("Mozilla/5.0 (%1) ").arg(args&: supp)
1753 + QLatin1String("KIO/%1.%2 ").arg(args: QString::number(KIO_VERSION_MAJOR), args: QString::number(KIO_VERSION_MINOR))
1754 + QLatin1String("%1/%2").arg(args&: appName, args&: appVersion);
1755
1756 return m_defaultUserAgent;
1757}
1758
1759bool HTTPProtocol::getSystemNameVersionAndMachine(QString &systemName, QString &machine)
1760{
1761#if defined(Q_OS_WIN)
1762 systemName = QStringLiteral("Windows");
1763#else
1764 struct utsname unameBuf;
1765 if (0 != uname(name: &unameBuf)) {
1766 return false;
1767 }
1768 systemName = QString::fromUtf8(utf8: unameBuf.sysname);
1769 machine = QString::fromUtf8(utf8: unameBuf.machine);
1770#endif
1771 return true;
1772}
1773
1774#include "http.moc"
1775#include "moc_http.cpp"
1776

source code of kio/src/kioworkers/http/http.cpp