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

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