File indexing completed on 2024-05-12 03:56:46

0001 /*
0002     SPDX-FileCopyrightText: 2000-2003 Waldo Bastian <bastian@kde.org>
0003     SPDX-FileCopyrightText: 2000-2002 George Staikos <staikos@kde.org>
0004     SPDX-FileCopyrightText: 2000-2002 Dawit Alemayehu <adawit@kde.org>
0005     SPDX-FileCopyrightText: 2001, 2002 Hamish Rodda <rodda@kde.org>
0006     SPDX-FileCopyrightText: 2007 Nick Shaforostoff <shafff@ukr.net>
0007     SPDX-FileCopyrightText: 2007-2018 Daniel Nicoletti <dantti12@gmail.com>
0008     SPDX-FileCopyrightText: 2008, 2009 Andreas Hartmetz <ahartmetz@gmail.com>
0009     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0010     SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
0011 
0012     SPDX-License-Identifier: LGPL-2.0-or-later
0013 */
0014 
0015 #include "http.h"
0016 #include "debug.h"
0017 #include "kioglobal_p.h"
0018 
0019 #include <QAuthenticator>
0020 #include <QBuffer>
0021 #include <QCoreApplication>
0022 #include <QDomDocument>
0023 #include <QMimeDatabase>
0024 #include <QNetworkAccessManager>
0025 #include <QNetworkCookie>
0026 #include <QNetworkCookieJar>
0027 #include <QNetworkProxy>
0028 #include <QNetworkReply>
0029 #include <QSslCipher>
0030 
0031 #include <KLocalizedString>
0032 
0033 #include <authinfo.h>
0034 
0035 // Pseudo plugin class to embed meta data
0036 class KIOPluginForMetaData : public QObject
0037 {
0038     Q_OBJECT
0039     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.http" FILE "http.json")
0040 };
0041 
0042 extern "C" {
0043 int Q_DECL_EXPORT kdemain(int argc, char **argv)
0044 {
0045     QCoreApplication app(argc, argv);
0046     app.setApplicationName(QStringLiteral("kio_http"));
0047 
0048     // start the worker
0049     HTTPProtocol worker(argv[1], argv[2], argv[3]);
0050     worker.dispatchLoop();
0051     return 0;
0052 }
0053 }
0054 
0055 class Cookies : public QNetworkCookieJar
0056 {
0057     Q_OBJECT
0058 public:
0059     Q_SIGNAL void cookiesAdded(const QString &cookieString);
0060     Q_SIGNAL void queryCookies(QString &cookieString);
0061 
0062     QList<QNetworkCookie> m_cookies;
0063 
0064     QList<QNetworkCookie> cookiesForUrl(const QUrl & /*url*/) const override
0065     {
0066         return m_cookies;
0067     }
0068 
0069     bool setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl & /*url*/) override
0070     {
0071         QString cookieString;
0072 
0073         for (const QNetworkCookie &cookie : cookieList) {
0074             cookieString += QStringLiteral("Set-Cookie: ") + QString::fromUtf8(cookie.toRawForm()) + QLatin1Char('\n');
0075         }
0076 
0077         Q_EMIT cookiesAdded(cookieString);
0078 
0079         return true;
0080     }
0081 
0082     void setCookies(const QString &cookieString)
0083     {
0084         const QStringList cookiePieces = cookieString.mid(8).split(QLatin1Char(';'), Qt::SkipEmptyParts);
0085 
0086         for (const QString &cookiePiece : cookiePieces) {
0087             const QString name = cookiePiece.left(cookiePiece.indexOf(QLatin1Char('=')));
0088             const QString value = cookiePiece.mid(cookiePiece.indexOf(QLatin1Char('=')) + 1);
0089 
0090             QNetworkCookie cookie(name.toUtf8(), value.toUtf8());
0091             m_cookies << cookie;
0092         }
0093     }
0094 };
0095 
0096 HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app)
0097     : WorkerBase(protocol, pool, app)
0098 {
0099 }
0100 
0101 HTTPProtocol::~HTTPProtocol()
0102 {
0103 }
0104 
0105 QString readMimeType(QNetworkReply *reply)
0106 {
0107     const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0108 
0109     return contentType.left(contentType.indexOf(QLatin1Char(';')));
0110 }
0111 
0112 void HTTPProtocol::handleSslErrors(QNetworkReply *reply, const QList<QSslError> errors)
0113 {
0114     if (!metaData(QStringLiteral("ssl_no_ui")).isEmpty() && metaData(QStringLiteral("ssl_no_ui")).compare(QLatin1String("false"), Qt::CaseInsensitive)) {
0115         return;
0116     }
0117 
0118     QList<QSslCertificate> certs = reply->sslConfiguration().peerCertificateChain();
0119 
0120     QStringList peerCertChain;
0121     for (const QSslCertificate &cert : certs) {
0122         peerCertChain += QString::fromUtf8(cert.toPem());
0123     }
0124 
0125     auto sslErrors = errors;
0126 
0127     // const QList<QSslCertificate> peerCertificateChain = socket.peerCertificateChain();
0128     // try to fill in the blanks, i.e. missing certificates, and just assume that
0129     // those belong to the peer (==website or similar) certificate.
0130     for (int i = 0; i < sslErrors.count(); i++) {
0131         if (sslErrors[i].certificate().isNull()) {
0132             sslErrors[i] = QSslError(sslErrors[i].error(), certs[0]);
0133         }
0134     }
0135 
0136     QStringList certificateErrors;
0137     // encode the two-dimensional numeric error list using '\n' and '\t' as outer and inner separators
0138     for (const QSslCertificate &cert : certs) {
0139         QString errorStr;
0140         for (const QSslError &error : std::as_const(sslErrors)) {
0141             if (error.certificate() == cert) {
0142                 errorStr = QString::number(static_cast<int>(error.error())) + QLatin1Char('\t');
0143             }
0144         }
0145         if (errorStr.endsWith(QLatin1Char('\t'))) {
0146             errorStr.chop(1);
0147         }
0148         certificateErrors << errorStr;
0149     }
0150 
0151     const QSslCipher cipher = reply->sslConfiguration().sessionCipher();
0152 
0153     const QVariantMap sslData{
0154         {QStringLiteral("hostname"), m_hostName},
0155         {QStringLiteral("protocol"), cipher.protocolString()},
0156         {QStringLiteral("sslError"), errors.first().errorString()},
0157         {QStringLiteral("peerCertChain"), peerCertChain},
0158         {QStringLiteral("certificateErrors"), certificateErrors},
0159         {QStringLiteral("cipher"), cipher.name()},
0160         {QStringLiteral("bits"), cipher.supportedBits()},
0161         {QStringLiteral("usedBits"), cipher.usedBits()},
0162     };
0163 
0164     int result = sslError(sslData);
0165 
0166     if (result == 1) {
0167         reply->ignoreSslErrors();
0168     } else {
0169         Q_EMIT errorOut(KIO::ERR_CANNOT_CONNECT);
0170     }
0171 }
0172 
0173 HTTPProtocol::Response HTTPProtocol::makeDavRequest(const QUrl &url,
0174                                                     KIO::HTTP_METHOD method,
0175                                                     QByteArray &inputData,
0176                                                     DataMode dataMode,
0177                                                     const QMap<QByteArray, QByteArray> &extraHeaders)
0178 {
0179     auto headers = extraHeaders;
0180     const QString locks = davProcessLocks();
0181 
0182     if (!headers.contains("Content-Type")) {
0183         headers.insert("Content-Type", "text/xml; charset=utf-8");
0184     }
0185 
0186     if (!locks.isEmpty()) {
0187         headers.insert("If", locks.toLatin1());
0188     }
0189 
0190     return makeRequest(url, method, inputData, dataMode, headers);
0191 }
0192 
0193 HTTPProtocol::Response
0194 HTTPProtocol::makeRequest(const QUrl &url, KIO::HTTP_METHOD method, QByteArray &inputData, DataMode dataMode, const QMap<QByteArray, QByteArray> &extraHeaders)
0195 {
0196     QBuffer buffer(&inputData);
0197     return makeRequest(url, method, &buffer, dataMode, extraHeaders);
0198 }
0199 
0200 static QString protocolForProxyType(QNetworkProxy::ProxyType type)
0201 {
0202     switch (type) {
0203     case QNetworkProxy::DefaultProxy:
0204         break;
0205     case QNetworkProxy::Socks5Proxy:
0206         return QStringLiteral("socks");
0207     case QNetworkProxy::NoProxy:
0208         break;
0209     case QNetworkProxy::HttpProxy:
0210     case QNetworkProxy::HttpCachingProxy:
0211     case QNetworkProxy::FtpCachingProxy:
0212         break;
0213     }
0214 
0215     return QStringLiteral("http");
0216 }
0217 
0218 HTTPProtocol::Response HTTPProtocol::makeRequest(const QUrl &url,
0219                                                  KIO::HTTP_METHOD method,
0220                                                  QIODevice *inputData,
0221                                                  HTTPProtocol::DataMode dataMode,
0222                                                  const QMap<QByteArray, QByteArray> &extraHeaders)
0223 {
0224     QNetworkAccessManager nam;
0225 
0226     // Disable automatic redirect handling from Qt. We need to intercept redirects
0227     // to let KIO handle them
0228     nam.setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
0229 
0230     auto cookies = new Cookies;
0231 
0232     if (metaData(QStringLiteral("cookies")) == QStringLiteral("manual")) {
0233         cookies->setCookies(metaData(QStringLiteral("setcookies")));
0234 
0235         connect(cookies, &Cookies::cookiesAdded, this, [this](const QString &cookiesString) {
0236             setMetaData(QStringLiteral("setcookies"), cookiesString);
0237         });
0238     }
0239 
0240     nam.setCookieJar(cookies);
0241 
0242     QUrl properUrl = url;
0243     if (url.scheme() == QLatin1String("webdav")) {
0244         properUrl.setScheme(QStringLiteral("http"));
0245     }
0246     if (url.scheme() == QLatin1String("webdavs")) {
0247         properUrl.setScheme(QStringLiteral("https"));
0248     }
0249 
0250     m_hostName = properUrl.host();
0251 
0252     connect(&nam, &QNetworkAccessManager::authenticationRequired, this, [this, url](QNetworkReply * /*reply*/, QAuthenticator *authenticator) {
0253         if (configValue(QStringLiteral("no-www-auth"), false)) {
0254             return;
0255         }
0256 
0257         KIO::AuthInfo authinfo;
0258         authinfo.url = url;
0259         authinfo.username = url.userName();
0260         authinfo.prompt = i18n(
0261             "You need to supply a username and a "
0262             "password to access this site.");
0263         authinfo.commentLabel = i18n("Site:");
0264 
0265         // try to get credentials from kpasswdserver's cache, then try asking the user.
0266         authinfo.verifyPath = false; // we have realm, no path based checking please!
0267         authinfo.realmValue = authenticator->realm();
0268 
0269         // Save the current authinfo url because it can be modified by the call to
0270         // checkCachedAuthentication. That way we can restore it if the call
0271         // modified it.
0272         const QUrl reqUrl = authinfo.url;
0273 
0274         if (checkCachedAuthentication(authinfo)) {
0275             authenticator->setUser(authinfo.username);
0276             authenticator->setPassword(authinfo.password);
0277         } else {
0278             // Reset url to the saved url...
0279             authinfo.url = reqUrl;
0280             authinfo.keepPassword = true;
0281             authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host());
0282 
0283             const int errorCode = openPasswordDialog(authinfo, QString());
0284 
0285             if (!errorCode) {
0286                 authenticator->setUser(authinfo.username);
0287                 authenticator->setPassword(authinfo.password);
0288                 if (authinfo.keepPassword) {
0289                     cacheAuthentication(authinfo);
0290                 }
0291             }
0292         }
0293     });
0294 
0295     connect(&nam, &QNetworkAccessManager::proxyAuthenticationRequired, this, [this](const QNetworkProxy &proxy, QAuthenticator *authenticator) {
0296         if (configValue(QStringLiteral("no-proxy-auth"), false)) {
0297             return;
0298         }
0299 
0300         QUrl proxyUrl;
0301 
0302         proxyUrl.setScheme(protocolForProxyType(proxy.type()));
0303         proxyUrl.setUserName(proxy.user());
0304         proxyUrl.setHost(proxy.hostName());
0305         proxyUrl.setPort(proxy.port());
0306 
0307         KIO::AuthInfo authinfo;
0308         authinfo.url = proxyUrl;
0309         authinfo.username = proxyUrl.userName();
0310         authinfo.prompt = i18n(
0311             "You need to supply a username and a password for "
0312             "the proxy server listed below before you are allowed "
0313             "to access any sites.");
0314         authinfo.keepPassword = true;
0315         authinfo.commentLabel = i18n("Proxy:");
0316 
0317         // try to get credentials from kpasswdserver's cache, then try asking the user.
0318         authinfo.verifyPath = false; // we have realm, no path based checking please!
0319         authinfo.realmValue = authenticator->realm();
0320         authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), proxyUrl.host());
0321 
0322         // Save the current authinfo url because it can be modified by the call to
0323         // checkCachedAuthentication. That way we can restore it if the call
0324         // modified it.
0325         const QUrl reqUrl = authinfo.url;
0326 
0327         if (checkCachedAuthentication(authinfo)) {
0328             authenticator->setUser(authinfo.username);
0329             authenticator->setPassword(authinfo.password);
0330         } else {
0331             // Reset url to the saved url...
0332             authinfo.url = reqUrl;
0333             authinfo.keepPassword = true;
0334             authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host());
0335 
0336             const int errorCode = openPasswordDialog(authinfo, QString());
0337 
0338             if (!errorCode) {
0339                 authenticator->setUser(authinfo.username);
0340                 authenticator->setPassword(authinfo.password);
0341                 if (authinfo.keepPassword) {
0342                     cacheAuthentication(authinfo);
0343                 }
0344             }
0345         }
0346     });
0347 
0348     QNetworkRequest request(properUrl);
0349 
0350     const QByteArray contentType = getContentType().toUtf8();
0351 
0352     if (!contentType.isEmpty()) {
0353         request.setHeader(QNetworkRequest::ContentTypeHeader, contentType);
0354     }
0355 
0356     const QString referrer = metaData(QStringLiteral("referrer"));
0357     if (!referrer.isEmpty()) {
0358         request.setRawHeader("Referer" /* sic! */, referrer.toUtf8());
0359     }
0360 
0361     const QString userAgent = metaData(QStringLiteral("UserAgent"));
0362     if (!userAgent.isEmpty()) {
0363         request.setHeader(QNetworkRequest::UserAgentHeader, userAgent.toUtf8());
0364     }
0365 
0366     const QString accept = metaData(QStringLiteral("accept"));
0367     if (!accept.isEmpty()) {
0368         request.setRawHeader("Accept", accept.toUtf8());
0369     }
0370 
0371     for (auto [key, value] : extraHeaders.asKeyValueRange()) {
0372         request.setRawHeader(key, value);
0373     }
0374 
0375     const QString customHeaders = metaData(QStringLiteral("customHTTPHeader"));
0376     if (!customHeaders.isEmpty()) {
0377         const QStringList headers = customHeaders.split(QLatin1String("\r\n"));
0378 
0379         for (const QString &header : headers) {
0380             const QStringList split = header.split(QLatin1String(": "));
0381             Q_ASSERT(split.size() == 2);
0382 
0383             request.setRawHeader(split[0].toUtf8(), split[1].toUtf8());
0384         }
0385     }
0386 
0387     QNetworkReply *reply = nam.sendCustomRequest(request, methodToString(method), inputData);
0388 
0389     bool mimeTypeEmitted = false;
0390     bool hadData = false;
0391 
0392     QEventLoop loop;
0393 
0394     QObject::connect(reply, &QNetworkReply::sslErrors, &loop, [this, reply](const QList<QSslError> errors) {
0395         handleSslErrors(reply, errors);
0396     });
0397 
0398     qint64 lastTotalSize = -1;
0399 
0400     QObject::connect(reply, &QNetworkReply::downloadProgress, this, [this, &lastTotalSize](qint64 received, qint64 total) {
0401         if (total != lastTotalSize) {
0402             lastTotalSize = total;
0403             totalSize(total);
0404         }
0405 
0406         processedSize(received);
0407     });
0408 
0409     QObject::connect(reply, &QNetworkReply::metaDataChanged, [this, &mimeTypeEmitted, reply, dataMode, url, method]() {
0410         handleRedirection(method, url, reply);
0411 
0412         if (!mimeTypeEmitted) {
0413             mimeType(readMimeType(reply));
0414             mimeTypeEmitted = true;
0415         }
0416 
0417         if (dataMode == Emit) {
0418             // Limit how much data we fetch at a time to avoid storing it all in RAM
0419             // do it in metaDataChanged to work around https://bugreports.qt.io/browse/QTBUG-15065
0420             reply->setReadBufferSize(2048);
0421         }
0422     });
0423 
0424     if (dataMode == Emit) {
0425         QObject::connect(reply, &QNetworkReply::readyRead, &nam, [this, reply, &hadData] {
0426             while (reply->bytesAvailable() > 0) {
0427                 QByteArray buf(2048, Qt::Uninitialized);
0428                 qint64 readBytes = reply->read(buf.data(), 2048);
0429                 buf.truncate(readBytes);
0430                 data(buf);
0431                 hadData = true;
0432             }
0433         });
0434     }
0435 
0436     QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
0437     QObject::connect(this, &HTTPProtocol::errorOut, &loop, [this, &loop](KIO::Error error) {
0438         lastError = error;
0439         loop.quit();
0440     });
0441     loop.exec();
0442 
0443     // make sure data is emitted at least once
0444     if (!hadData && dataMode == Emit) {
0445         data(QByteArray());
0446     }
0447 
0448     if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
0449         reply->deleteLater();
0450         return {0, QByteArray(), KIO::ERR_ACCESS_DENIED};
0451     }
0452 
0453     if (configValue(QStringLiteral("PropagateHttpHeader"), false)) {
0454         QStringList headers;
0455 
0456         const auto headerPairs = reply->rawHeaderPairs();
0457         for (auto [key, value] : headerPairs) {
0458             headers << QString::fromLatin1(key + ": " + value);
0459         }
0460 
0461         setMetaData(QStringLiteral("HTTP-Headers"), headers.join(QLatin1Char('\n')));
0462     }
0463 
0464     QByteArray returnData;
0465 
0466     if (dataMode == Return) {
0467         returnData = reply->readAll();
0468     }
0469 
0470     int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0471 
0472     setMetaData(QStringLiteral("responsecode"), QString::number(statusCode));
0473     setMetaData(QStringLiteral("content-type"), readMimeType(reply));
0474 
0475     reply->deleteLater();
0476 
0477     return {statusCode, returnData};
0478 }
0479 
0480 KIO::WorkerResult HTTPProtocol::get(const QUrl &url)
0481 {
0482     QByteArray inputData = getData();
0483     Response response = makeRequest(url, KIO::HTTP_GET, inputData, DataMode::Emit);
0484 
0485     return sendHttpError(url, KIO::HTTP_GET, response);
0486 }
0487 
0488 KIO::WorkerResult HTTPProtocol::put(const QUrl &url, int /*_mode*/, KIO::JobFlags flags)
0489 {
0490     if (url.scheme().startsWith(QLatin1String("webdav"))) {
0491         if (!(flags & KIO::Overwrite)) {
0492             // Checks if the destination exists and return an error if it does.
0493             if (davDestinationExists(url)) {
0494                 return KIO::WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, url.fileName());
0495             }
0496         }
0497     }
0498 
0499     QByteArray inputData = getData();
0500     Response response = makeRequest(url, KIO::HTTP_PUT, inputData, DataMode::Emit);
0501 
0502     return sendHttpError(url, KIO::HTTP_PUT, response);
0503 }
0504 
0505 KIO::WorkerResult HTTPProtocol::mimetype(const QUrl &url)
0506 {
0507     QByteArray inputData = getData();
0508     Response response = makeRequest(url, KIO::HTTP_HEAD, inputData, DataMode::Discard);
0509 
0510     return sendHttpError(url, KIO::HTTP_HEAD, response);
0511 }
0512 
0513 KIO::WorkerResult HTTPProtocol::post(const QUrl &url, qint64 /*size*/)
0514 {
0515     QByteArray inputData = getData();
0516     Response response = makeRequest(url, KIO::HTTP_POST, inputData, DataMode::Emit);
0517 
0518     return sendHttpError(url, KIO::HTTP_POST, response);
0519 }
0520 
0521 KIO::WorkerResult HTTPProtocol::special(const QByteArray &data)
0522 {
0523     int tmp;
0524     QDataStream stream(data);
0525 
0526     stream >> tmp;
0527     switch (tmp) {
0528     case 1: { // HTTP POST
0529         QUrl url;
0530         qint64 size;
0531         stream >> url >> size;
0532         return post(url, size);
0533     }
0534     case 7: { // Generic WebDAV
0535         QUrl url;
0536         int method;
0537         qint64 size;
0538         stream >> url >> method >> size;
0539         return davGeneric(url, (KIO::HTTP_METHOD)method, size);
0540     }
0541     }
0542     return KIO::WorkerResult::pass();
0543 }
0544 
0545 QByteArray HTTPProtocol::getData()
0546 {
0547     // TODO this is probably not great. Instead create a QIODevice that calls readData and pass that to QNAM?
0548     QByteArray dataBuffer;
0549 
0550     while (true) {
0551         dataReq();
0552 
0553         QByteArray buffer;
0554         const int bytesRead = readData(buffer);
0555 
0556         dataBuffer += buffer;
0557 
0558         // On done...
0559         if (bytesRead == 0) {
0560             // sendOk = (bytesSent == m_iPostDataSize);
0561             break;
0562         }
0563     }
0564 
0565     return dataBuffer;
0566 }
0567 
0568 QString HTTPProtocol::getContentType()
0569 {
0570     QString contentType = metaData(QStringLiteral("content-type"));
0571     if (contentType.startsWith(QLatin1String("Content-Type: "), Qt::CaseInsensitive)) {
0572         contentType.remove(QLatin1String("Content-Type: "), Qt::CaseInsensitive);
0573     }
0574     return contentType;
0575 }
0576 
0577 void HTTPProtocol::handleRedirection(KIO::HTTP_METHOD method, const QUrl &originalUrl, QNetworkReply *reply)
0578 {
0579     int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0580 
0581     auto redirect = [this, originalUrl, reply] {
0582         const QString redir = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
0583         redirection(originalUrl.resolved(QUrl(redir)));
0584     };
0585 
0586     if (statusCode == 301) {
0587         setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true"));
0588         redirect();
0589     } else if (statusCode == 302) {
0590         if (method == KIO::HTTP_POST) {
0591             setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true"));
0592         }
0593 
0594         redirect();
0595     } else if (statusCode == 303) {
0596         if (method != KIO::HTTP_HEAD) {
0597             setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true"));
0598         }
0599 
0600         redirect();
0601     } else if (statusCode == 307) {
0602         redirect();
0603     } else if (statusCode == 308) {
0604         setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true"));
0605         redirect();
0606     }
0607 }
0608 
0609 KIO::WorkerResult HTTPProtocol::listDir(const QUrl &url)
0610 {
0611     return davStatList(url, false);
0612 }
0613 
0614 KIO::WorkerResult HTTPProtocol::davStatList(const QUrl &url, bool stat)
0615 {
0616     KIO::UDSEntry entry;
0617 
0618     QMimeDatabase db;
0619 
0620     KIO::HTTP_METHOD method;
0621     QByteArray inputData;
0622 
0623     // Maybe it's a disguised SEARCH...
0624     QString query = metaData(QStringLiteral("davSearchQuery"));
0625     if (!query.isEmpty()) {
0626         inputData =
0627             "<?xml version=\"1.0\"?>\r\n"
0628             "<D:searchrequest xmlns:D=\"DAV:\">\r\n"
0629             + query.toUtf8() + "</D:searchrequest>\r\n";
0630 
0631         method = KIO::DAV_SEARCH;
0632     } else {
0633         // We are only after certain features...
0634         inputData =
0635             "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
0636             "<D:propfind xmlns:D=\"DAV:\">"
0637             "<D:prop>"
0638             "<D:creationdate/>"
0639             "<D:getcontentlength/>"
0640             "<D:displayname/>"
0641             "<D:source/>"
0642             "<D:getcontentlanguage/>"
0643             "<D:getcontenttype/>"
0644             "<D:getlastmodified/>"
0645             "<D:getetag/>"
0646             "<D:supportedlock/>"
0647             "<D:lockdiscovery/>"
0648             "<D:resourcetype/>"
0649             "<D:quota-available-bytes/>"
0650             "<D:quota-used-bytes/>"
0651             "</D:prop>"
0652             "</D:propfind>";
0653         method = KIO::DAV_PROPFIND;
0654     }
0655 
0656     const QMap<QByteArray, QByteArray> extraHeaders = {
0657         {"Depth", stat ? "0" : "1"},
0658     };
0659 
0660     Response response = makeDavRequest(url, method, inputData, DataMode::Return, extraHeaders);
0661 
0662     // TODO
0663     // if (!stat) {
0664     // Utils::appendSlashToPath(m_request.url);
0665     // }
0666 
0667     // Has a redirection already been called? If so, we're done.
0668     // if (m_isRedirection || m_kioError) {
0669     // if (m_isRedirection) {
0670     // return davFinished();
0671     // }
0672     // return WorkerResult::pass();
0673     // }
0674 
0675     QDomDocument multiResponse;
0676     multiResponse.setContent(response.data, QDomDocument::ParseOption::UseNamespaceProcessing);
0677 
0678     bool hasResponse = false;
0679 
0680     for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) {
0681         QDomElement thisResponse = n.toElement();
0682         if (thisResponse.isNull()) {
0683             continue;
0684         }
0685 
0686         hasResponse = true;
0687 
0688         QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement();
0689         if (!href.isNull()) {
0690             entry.clear();
0691 
0692             const QUrl thisURL(href.text()); // href.text() is a percent-encoded url.
0693             if (thisURL.isValid()) {
0694                 const QUrl adjustedThisURL = thisURL.adjusted(QUrl::StripTrailingSlash);
0695                 const QUrl adjustedUrl = url.adjusted(QUrl::StripTrailingSlash);
0696 
0697                 // base dir of a listDir(): name should be "."
0698                 QString name;
0699                 if (!stat && adjustedThisURL.path() == adjustedUrl.path()) {
0700                     name = QLatin1Char('.');
0701                 } else {
0702                     name = adjustedThisURL.fileName();
0703                 }
0704 
0705                 entry.fastInsert(KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name);
0706             }
0707 
0708             QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat"));
0709 
0710             davParsePropstats(propstats, entry);
0711 
0712             // Since a lot of webdav servers seem not to send the content-type information
0713             // for the requested directory listings, we attempt to guess the MIME type from
0714             // the resource name so long as the resource is not a directory.
0715             if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) {
0716                 QMimeType mime = db.mimeTypeForFile(thisURL.path(), QMimeDatabase::MatchExtension);
0717                 if (mime.isValid() && !mime.isDefault()) {
0718                     // qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed MIME type for" << thisURL.path();
0719                     entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime.name());
0720                 }
0721             }
0722 
0723             if (stat) {
0724                 // return an item
0725                 statEntry(entry);
0726                 return KIO::WorkerResult::pass();
0727             }
0728             listEntry(entry);
0729         } else {
0730             // qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url;
0731         }
0732     }
0733 
0734     if (stat || !hasResponse) {
0735         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0736     }
0737 
0738     return KIO::WorkerResult::pass();
0739 }
0740 
0741 void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, KIO::UDSEntry &entry)
0742 {
0743     QString mimeType;
0744     bool foundExecutable = false;
0745     bool isDirectory = false;
0746     uint lockCount = 0;
0747     uint supportedLockCount = 0;
0748     qlonglong quotaUsed = -1;
0749     qlonglong quotaAvailable = -1;
0750 
0751     for (int i = 0; i < propstats.count(); i++) {
0752         QDomElement propstat = propstats.item(i).toElement();
0753 
0754         QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement();
0755         if (status.isNull()) {
0756             // error, no status code in this propstat
0757             // qCDebug(KIO_HTTP) << "Error, no status code in this propstat";
0758             return;
0759         }
0760 
0761         int code = codeFromResponse(status.text());
0762 
0763         if (code != 200) {
0764             // qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)";
0765             continue;
0766         }
0767 
0768         QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement();
0769         if (prop.isNull()) {
0770             // qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat.";
0771             return;
0772         }
0773 
0774         // TODO unnecessary?
0775         if (hasMetaData(QStringLiteral("davRequestResponse"))) {
0776             QDomDocument doc;
0777             doc.appendChild(prop);
0778             entry.replace(KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString());
0779         }
0780 
0781         for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) {
0782             QDomElement property = n.toElement();
0783             if (property.isNull()) {
0784                 continue;
0785             }
0786 
0787             if (property.namespaceURI() != QLatin1String("DAV:")) {
0788                 // break out - we're only interested in properties from the DAV namespace
0789                 continue;
0790             }
0791 
0792             if (property.tagName() == QLatin1String("creationdate")) {
0793                 // Resource creation date. Should be is ISO 8601 format.
0794                 entry.replace(KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch());
0795             } else if (property.tagName() == QLatin1String("getcontentlength")) {
0796                 // Content length (file size)
0797                 entry.replace(KIO::UDSEntry::UDS_SIZE, property.text().toULong());
0798             } else if (property.tagName() == QLatin1String("displayname")) {
0799                 // Name suitable for presentation to the user
0800                 setMetaData(QStringLiteral("davDisplayName"), property.text());
0801             } else if (property.tagName() == QLatin1String("source")) {
0802                 // Source template location
0803                 QDomElement source = property.namedItem(QStringLiteral("link")).toElement().namedItem(QStringLiteral("dst")).toElement();
0804                 if (!source.isNull()) {
0805                     setMetaData(QStringLiteral("davSource"), source.text());
0806                 }
0807             } else if (property.tagName() == QLatin1String("getcontentlanguage")) {
0808                 // equiv. to Content-Language header on a GET
0809                 setMetaData(QStringLiteral("davContentLanguage"), property.text());
0810             } else if (property.tagName() == QLatin1String("getcontenttype")) {
0811                 // Content type (MIME type)
0812                 // This may require adjustments for other server-side webdav implementations
0813                 // (tested with Apache + mod_dav 1.0.3)
0814                 if (property.text() == QLatin1String("httpd/unix-directory")) {
0815                     isDirectory = true;
0816                 } else if (property.text() != QLatin1String("application/octet-stream")) {
0817                     // The server could be lazy and always return application/octet-stream;
0818                     // we will guess the MIME type later in that case.
0819                     mimeType = property.text();
0820                 }
0821             } else if (property.tagName() == QLatin1String("executable")) {
0822                 // File executable status
0823                 if (property.text() == QLatin1Char('T')) {
0824                     foundExecutable = true;
0825                 }
0826 
0827             } else if (property.tagName() == QLatin1String("getlastmodified")) {
0828                 // Last modification date
0829                 entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME,
0830                               parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch());
0831             } else if (property.tagName() == QLatin1String("getetag")) {
0832                 // Entity tag
0833                 setMetaData(QStringLiteral("davEntityTag"), property.text());
0834             } else if (property.tagName() == QLatin1String("supportedlock")) {
0835                 // Supported locking specifications
0836                 for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) {
0837                     QDomElement lockEntry = n2.toElement();
0838                     if (lockEntry.tagName() == QLatin1String("lockentry")) {
0839                         QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement();
0840                         QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement();
0841                         if (!lockScope.isNull() && !lockType.isNull()) {
0842                             // Lock type was properly specified
0843                             supportedLockCount++;
0844                             const QString lockCountStr = QString::number(supportedLockCount);
0845                             const QString scope = lockScope.firstChild().toElement().tagName();
0846                             const QString type = lockType.firstChild().toElement().tagName();
0847 
0848                             setMetaData(QLatin1String("davSupportedLockScope") + lockCountStr, scope);
0849                             setMetaData(QLatin1String("davSupportedLockType") + lockCountStr, type);
0850                         }
0851                     }
0852                 }
0853             } else if (property.tagName() == QLatin1String("lockdiscovery")) {
0854                 // Lists the available locks
0855                 davParseActiveLocks(property.elementsByTagName(QStringLiteral("activelock")), lockCount);
0856             } else if (property.tagName() == QLatin1String("resourcetype")) {
0857                 // Resource type. "Specifies the nature of the resource."
0858                 if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) {
0859                     // This is a collection (directory)
0860                     isDirectory = true;
0861                 }
0862             } else if (property.tagName() == QLatin1String("quota-used-bytes")) {
0863                 // Quota-used-bytes. "Contains the amount of storage already in use."
0864                 quotaUsed = property.text().toLongLong();
0865             } else if (property.tagName() == QLatin1String("quota-available-bytes")) {
0866                 // Quota-available-bytes. "Indicates the maximum amount of additional storage available."
0867                 quotaAvailable = property.text().toLongLong();
0868             } else {
0869                 // qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName();
0870             }
0871         }
0872     }
0873 
0874     setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount));
0875     setMetaData(QStringLiteral("davSupportedLockCount"), QString::number(supportedLockCount));
0876 
0877     entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG);
0878 
0879     if (foundExecutable || isDirectory) {
0880         // File was executable, or is a directory.
0881         entry.replace(KIO::UDSEntry::UDS_ACCESS, 0700);
0882     } else {
0883         entry.replace(KIO::UDSEntry::UDS_ACCESS, 0600);
0884     }
0885 
0886     if (!isDirectory && !mimeType.isEmpty()) {
0887         entry.replace(KIO::UDSEntry::UDS_MIME_TYPE, mimeType);
0888     }
0889 
0890     if (quotaUsed >= 0 && quotaAvailable >= 0) {
0891         // Only used and available storage properties exist, the total storage size has to be calculated.
0892         setMetaData(QStringLiteral("total"), QString::number(quotaUsed + quotaAvailable));
0893         setMetaData(QStringLiteral("available"), QString::number(quotaAvailable));
0894     }
0895 }
0896 
0897 void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount)
0898 {
0899     for (int i = 0; i < activeLocks.count(); i++) {
0900         const QDomElement activeLock = activeLocks.item(i).toElement();
0901 
0902         lockCount++;
0903         // required
0904         const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement();
0905         const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement();
0906         const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement();
0907         // optional
0908         const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement();
0909         const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement();
0910         const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement();
0911 
0912         if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) {
0913             // lock was properly specified
0914             lockCount++;
0915             const QString lockCountStr = QString::number(lockCount);
0916             const QString scope = lockScope.firstChild().toElement().tagName();
0917             const QString type = lockType.firstChild().toElement().tagName();
0918             const QString depth = lockDepth.text();
0919 
0920             setMetaData(QLatin1String("davLockScope") + lockCountStr, scope);
0921             setMetaData(QLatin1String("davLockType") + lockCountStr, type);
0922             setMetaData(QLatin1String("davLockDepth") + lockCountStr, depth);
0923 
0924             if (!lockOwner.isNull()) {
0925                 setMetaData(QLatin1String("davLockOwner") + lockCountStr, lockOwner.text());
0926             }
0927 
0928             if (!lockTimeout.isNull()) {
0929                 setMetaData(QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text());
0930             }
0931 
0932             if (!lockToken.isNull()) {
0933                 QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement();
0934                 if (!tokenVal.isNull()) {
0935                     setMetaData(QLatin1String("davLockToken") + lockCountStr, tokenVal.text());
0936                 }
0937             }
0938         }
0939     }
0940 }
0941 
0942 QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type)
0943 {
0944     if (type == QLatin1String("dateTime.tz")) {
0945         return QDateTime::fromString(input, Qt::ISODate);
0946     } else if (type == QLatin1String("dateTime.rfc1123")) {
0947         return QDateTime::fromString(input, Qt::RFC2822Date);
0948     }
0949 
0950     // format not advertised... try to parse anyway
0951     QDateTime time = QDateTime::fromString(input, Qt::RFC2822Date);
0952     if (time.isValid()) {
0953         return time;
0954     }
0955 
0956     return QDateTime::fromString(input, Qt::ISODate);
0957 }
0958 
0959 int HTTPProtocol::codeFromResponse(const QString &response)
0960 {
0961     const int firstSpace = response.indexOf(QLatin1Char(' '));
0962     const int secondSpace = response.indexOf(QLatin1Char(' '), firstSpace + 1);
0963 
0964     return QStringView(response).mid(firstSpace + 1, secondSpace - firstSpace - 1).toInt();
0965 }
0966 
0967 KIO::WorkerResult HTTPProtocol::stat(const QUrl &url)
0968 {
0969     if (url.scheme() != QLatin1String("webdav") && url.scheme() != QLatin1String("webdavs")) {
0970         QString statSide = metaData(QStringLiteral("statSide"));
0971         if (statSide != QLatin1String("source")) {
0972             // When uploading we assume the file does not exist.
0973             return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0974         }
0975 
0976         // When downloading we assume it exists
0977         KIO::UDSEntry entry;
0978         entry.reserve(3);
0979         entry.fastInsert(KIO::UDSEntry::UDS_NAME, url.fileName());
0980         entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file
0981         entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody
0982 
0983         statEntry(entry);
0984         return KIO::WorkerResult::pass();
0985     }
0986 
0987     return davStatList(url, true);
0988 }
0989 
0990 KIO::WorkerResult HTTPProtocol::mkdir(const QUrl &url, int)
0991 {
0992     QByteArray inputData;
0993     Response response = makeDavRequest(url, KIO::DAV_MKCOL, inputData, DataMode::Discard);
0994 
0995     if (response.httpCode != 201) {
0996         return davError(KIO::DAV_MKCOL, url, response);
0997     }
0998     return KIO::WorkerResult::pass();
0999 }
1000 
1001 KIO::WorkerResult HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
1002 {
1003     QMap<QByteArray, QByteArray> extraHeaders = {
1004         {"Destination", dest.toString(QUrl::FullyEncoded).toUtf8()},
1005         {"Overwrite", (flags & KIO::Overwrite) ? "T" : "F"},
1006         {"Depth", "infinity"},
1007     };
1008 
1009     QByteArray inputData;
1010     Response response = makeDavRequest(src, KIO::DAV_MOVE, inputData, DataMode::Discard, extraHeaders);
1011 
1012     // Work around strict Apache-2 WebDAV implementation which refuses to cooperate
1013     // with webdav://host/directory, instead requiring webdav://host/directory/
1014     // (strangely enough it accepts Destination: without a trailing slash)
1015     // See BR# 209508 and BR#187970
1016     // TODO
1017     // if (m_request.responseCode == 301) {
1018     //     QUrl redir = m_request.redirectUrl;
1019     //
1020     //     resetSessionSettings();
1021     //
1022     //     m_request.url = redir;
1023     //     m_request.method = DAV_MOVE;
1024     //     m_request.davData.desturl = newDest.toString();
1025     //     m_request.davData.overwrite = (flags & KIO::Overwrite);
1026     //     m_request.url.setQuery(QString());
1027     //     m_request.cacheTag.policy = CC_Reload;
1028     //
1029     //     (void)/* handling result via dav codes */ proceedUntilResponseHeader();
1030     // }
1031 
1032     // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion
1033     if (response.httpCode == 201 || response.httpCode == 204) {
1034         return KIO::WorkerResult::pass();
1035     }
1036     return davError(KIO::DAV_MOVE, src, response);
1037 }
1038 
1039 KIO::WorkerResult HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags)
1040 {
1041     const bool isSourceLocal = src.isLocalFile();
1042     const bool isDestinationLocal = dest.isLocalFile();
1043 
1044     if (isSourceLocal && !isDestinationLocal) {
1045         return copyPut(src, dest, flags);
1046     }
1047 
1048     if (!(flags & KIO::Overwrite)) {
1049         // Checks if the destination exists and return an error if it does.
1050         if (davDestinationExists(dest)) {
1051             return KIO::WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, dest.fileName());
1052         }
1053     }
1054 
1055     QMap<QByteArray, QByteArray> extraHeaders = {
1056         {"Destination", dest.toString(QUrl::FullyEncoded).toUtf8()},
1057         {"Overwrite", (flags & KIO::Overwrite) ? "T" : "F"},
1058         {"Depth", "infinity"},
1059     };
1060 
1061     QByteArray inputData;
1062     Response response = makeDavRequest(src, KIO::DAV_COPY, inputData, DataMode::Discard, extraHeaders);
1063 
1064     // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion
1065     if (response.httpCode == 201 || response.httpCode == 204) {
1066         return KIO::WorkerResult::pass();
1067     }
1068 
1069     return davError(KIO::DAV_COPY, src, response);
1070 }
1071 
1072 KIO::WorkerResult HTTPProtocol::del(const QUrl &url, bool)
1073 {
1074     if (url.scheme().startsWith(QLatin1String("webdav"))) {
1075         Response response = makeRequest(url, KIO::HTTP_DELETE, {}, DataMode::Discard);
1076 
1077         // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content
1078         // on successful completion.
1079         if (response.httpCode == 200 || response.httpCode == 204 /*|| m_isRedirection*/) {
1080             return KIO::WorkerResult::pass();
1081         }
1082         return davError(KIO::HTTP_DELETE, url, response);
1083     }
1084 
1085     Response response = makeRequest(url, KIO::HTTP_DELETE, {}, DataMode::Discard);
1086 
1087     return sendHttpError(url, KIO::HTTP_DELETE, response);
1088 }
1089 
1090 KIO::WorkerResult HTTPProtocol::copyPut(const QUrl &src, const QUrl &dest, KIO::JobFlags flags)
1091 {
1092     if (!(flags & KIO::Overwrite)) {
1093         // Checks if the destination exists and return an error if it does.
1094         if (davDestinationExists(dest)) {
1095             return KIO::WorkerResult::fail(KIO::ERR_FILE_ALREADY_EXIST, dest.fileName());
1096         }
1097     }
1098 
1099     auto sourceFile = new QFile(src.toLocalFile());
1100     if (!sourceFile->open(QFile::ReadOnly)) {
1101         return KIO::WorkerResult::fail(KIO::ERR_CANNOT_OPEN_FOR_READING, src.fileName());
1102     }
1103 
1104     Response response = makeRequest(dest, KIO::HTTP_PUT, sourceFile, {});
1105 
1106     return sendHttpError(dest, KIO::HTTP_PUT, response);
1107 }
1108 
1109 bool HTTPProtocol::davDestinationExists(const QUrl &url)
1110 {
1111     QByteArray request(
1112         "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
1113         "<D:propfind xmlns:D=\"DAV:\"><D:prop>"
1114         "<D:creationdate/>"
1115         "<D:getcontentlength/>"
1116         "<D:displayname/>"
1117         "<D:resourcetype/>"
1118         "</D:prop></D:propfind>");
1119 
1120     const QMap<QByteArray, QByteArray> extraHeaders = {
1121         {"Depth", "0"},
1122     };
1123 
1124     Response response = makeDavRequest(url, KIO::DAV_PROPFIND, request, DataMode::Discard, extraHeaders);
1125 
1126     if (response.httpCode >= 200 && response.httpCode < 300) {
1127         // 2XX means the file exists. This includes 207 (multi-status response).
1128         // qCDebug(KIO_HTTP) << "davDestinationExists: file exists. code:" << m_request.responseCode;
1129         return true;
1130     } else {
1131         // qCDebug(KIO_HTTP) << "davDestinationExists: file does not exist. code:" << m_request.responseCode;
1132     }
1133 
1134     return false;
1135 }
1136 
1137 KIO::WorkerResult HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size)
1138 {
1139     // TODO what about size?
1140 
1141     QMap<QByteArray, QByteArray> extraHeaders;
1142 
1143     if (method == KIO::DAV_PROPFIND || method == KIO::DAV_REPORT) {
1144         int depth = 0;
1145 
1146         if (hasMetaData(QStringLiteral("davDepth"))) {
1147             depth = metaData(QStringLiteral("davDepth")).toInt();
1148         } else {
1149             // TODO is warning here appropriate?
1150             qWarning() << "Performing DAV PROPFIND or REPORT without specifying davDepth";
1151         }
1152 
1153         extraHeaders.insert("Depth", QByteArray::number(depth));
1154     }
1155 
1156     QByteArray inputData = getData();
1157     Response response = makeDavRequest(url, method, inputData, DataMode::Emit, extraHeaders);
1158 
1159     // TODO old code seems to use http error, not dav error
1160     return sendHttpError(url, method, response);
1161 }
1162 
1163 KIO::WorkerResult HTTPProtocol::fileSystemFreeSpace(const QUrl &url)
1164 {
1165     return davStatList(url, true);
1166 }
1167 
1168 KIO::WorkerResult HTTPProtocol::davError(KIO::HTTP_METHOD method, const QUrl &url, const Response &response)
1169 {
1170     if (response.kioCode == KIO::ERR_ACCESS_DENIED) {
1171         return KIO::WorkerResult::fail(response.kioCode, url.toDisplayString());
1172     }
1173 
1174     QString discard;
1175     return davError(discard, method, response.httpCode, url, response.data);
1176 }
1177 
1178 QString HTTPProtocol::davProcessLocks()
1179 {
1180     if (hasMetaData(QStringLiteral("davLockCount"))) {
1181         QString response;
1182         int numLocks = metaData(QStringLiteral("davLockCount")).toInt();
1183         bool bracketsOpen = false;
1184         for (int i = 0; i < numLocks; i++) {
1185             const QString countStr = QString::number(i);
1186             if (hasMetaData(QLatin1String("davLockToken") + countStr)) {
1187                 if (hasMetaData(QLatin1String("davLockURL") + countStr)) {
1188                     if (bracketsOpen) {
1189                         response += QLatin1Char(')');
1190                         bracketsOpen = false;
1191                     }
1192                     response += QLatin1String(" <") + metaData(QLatin1String("davLockURL") + countStr) + QLatin1Char('>');
1193                 }
1194 
1195                 if (!bracketsOpen) {
1196                     response += QLatin1String(" (");
1197                     bracketsOpen = true;
1198                 } else {
1199                     response += QLatin1Char(' ');
1200                 }
1201 
1202                 if (hasMetaData(QLatin1String("davLockNot") + countStr)) {
1203                     response += QLatin1String("Not ");
1204                 }
1205 
1206                 response += QLatin1Char('<') + metaData(QLatin1String("davLockToken") + countStr) + QLatin1Char('>');
1207             }
1208         }
1209 
1210         if (bracketsOpen) {
1211             response += QLatin1Char(')');
1212         }
1213 
1214         return response;
1215     }
1216 
1217     return QString();
1218 }
1219 
1220 KIO::WorkerResult HTTPProtocol::davError(QString &errorMsg, KIO::HTTP_METHOD method, int code, const QUrl &url, const QByteArray &responseData)
1221 {
1222     QString action;
1223     QString errorString;
1224     int errorCode = KIO::ERR_WORKER_DEFINED;
1225 
1226     // for 412 Precondition Failed
1227     QString ow = i18n("Otherwise, the request would have succeeded.");
1228 
1229     if (method == KIO::DAV_PROPFIND) {
1230         action = i18nc("request type", "retrieve property values");
1231     } else if (method == KIO::DAV_PROPPATCH) {
1232         action = i18nc("request type", "set property values");
1233     } else if (method == KIO::DAV_MKCOL) {
1234         action = i18nc("request type", "create the requested folder");
1235     } else if (method == KIO::DAV_COPY) {
1236         action = i18nc("request type", "copy the specified file or folder");
1237     } else if (method == KIO::DAV_MOVE) {
1238         action = i18nc("request type", "move the specified file or folder");
1239     } else if (method == KIO::DAV_SEARCH) {
1240         action = i18nc("request type", "search in the specified folder");
1241     } else if (method == KIO::DAV_LOCK) {
1242         action = i18nc("request type", "lock the specified file or folder");
1243     } else if (method == KIO::DAV_UNLOCK) {
1244         action = i18nc("request type", "unlock the specified file or folder");
1245     } else if (method == KIO::HTTP_DELETE) {
1246         action = i18nc("request type", "delete the specified file or folder");
1247     } else if (method == KIO::HTTP_OPTIONS) {
1248         action = i18nc("request type", "query the server's capabilities");
1249     } else if (method == KIO::HTTP_GET) {
1250         action = i18nc("request type", "retrieve the contents of the specified file or folder");
1251     } else if (method == KIO::DAV_REPORT) {
1252         action = i18nc("request type", "run a report in the specified folder");
1253     } else {
1254         // this should not happen, this function is for webdav errors only
1255         Q_ASSERT(0);
1256     }
1257 
1258     // default error message if the following code fails
1259     errorString = i18nc("%1: code, %2: request type",
1260                         "An unexpected error (%1) occurred "
1261                         "while attempting to %2.",
1262                         code,
1263                         action);
1264 
1265     switch (code) {
1266     case 207:
1267         // 207 Multi-status
1268         {
1269             // our error info is in the returned XML document.
1270             // retrieve the XML document
1271 
1272             QStringList errors;
1273             QDomDocument multiResponse;
1274             multiResponse.setContent(responseData, QDomDocument::ParseOption::UseNamespaceProcessing);
1275 
1276             QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement();
1277 
1278             QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response"));
1279 
1280             for (int i = 0; i < responses.count(); i++) {
1281                 int errCode;
1282                 QUrl errUrl;
1283 
1284                 QDomElement response = responses.item(i).toElement();
1285                 QDomElement code = response.namedItem(QStringLiteral("status")).toElement();
1286 
1287                 if (!code.isNull()) {
1288                     errCode = codeFromResponse(code.text());
1289                     QDomElement href = response.namedItem(QStringLiteral("href")).toElement();
1290                     if (!href.isNull()) {
1291                         errUrl = QUrl(href.text());
1292                     }
1293                     QString error;
1294                     (void)davError(error, method, errCode, errUrl, {});
1295                     errors << error;
1296                 }
1297             }
1298 
1299             errorString = i18nc("%1: request type, %2: url",
1300                                 "An error occurred while attempting to %1, %2. A "
1301                                 "summary of the reasons is below.",
1302                                 action,
1303                                 url.toString());
1304 
1305             errorString += QLatin1String("<ul>");
1306 
1307             for (const QString &error : std::as_const(errors)) {
1308                 errorString += QLatin1String("<li>") + error + QLatin1String("</li>");
1309             }
1310 
1311             errorString += QLatin1String("</ul>");
1312         }
1313         break;
1314     case 403:
1315     case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1316         // 403 Forbidden
1317         // ERR_ACCESS_DENIED
1318         errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action);
1319         break;
1320     case 405:
1321         // 405 Method Not Allowed
1322         if (method == KIO::DAV_MKCOL) {
1323             // ERR_DIR_ALREADY_EXIST
1324             errorString = url.toString();
1325             errorCode = KIO::ERR_DIR_ALREADY_EXIST;
1326         }
1327         break;
1328     case 409:
1329         // 409 Conflict
1330         // ERR_ACCESS_DENIED
1331         errorString = i18n(
1332             "A resource cannot be created at the destination "
1333             "until one or more intermediate collections (folders) "
1334             "have been created.");
1335         break;
1336     case 412:
1337         // 412 Precondition failed
1338         if (method == KIO::DAV_COPY || method == KIO::DAV_MOVE) {
1339             // ERR_ACCESS_DENIED
1340             errorString = i18n(
1341                 "The server was unable to maintain the liveness of the\n"
1342                 "properties listed in the propertybehavior XML element\n"
1343                 "or you attempted to overwrite a file while requesting\n"
1344                 "that files are not overwritten.\n %1",
1345                 ow);
1346 
1347         } else if (method == KIO::DAV_LOCK) {
1348             // ERR_ACCESS_DENIED
1349             errorString = i18n("The requested lock could not be granted. %1", ow);
1350         }
1351         break;
1352     case 415:
1353         // 415 Unsupported Media Type
1354         // ERR_ACCESS_DENIED
1355         errorString = i18n("The server does not support the request type of the body.");
1356         break;
1357     case 423:
1358         // 423 Locked
1359         // ERR_ACCESS_DENIED
1360         errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action);
1361         break;
1362     case 425:
1363         // 424 Failed Dependency
1364         errorString = i18n("This action was prevented by another error.");
1365         break;
1366     case 502:
1367         // 502 Bad Gateway
1368         if (method == KIO::DAV_COPY || method == KIO::DAV_MOVE) {
1369             // ERR_WRITE_ACCESS_DENIED
1370             errorString = i18nc("%1: request type",
1371                                 "Unable to %1 because the destination server refuses "
1372                                 "to accept the file or folder.",
1373                                 action);
1374         }
1375         break;
1376     case 507:
1377         // 507 Insufficient Storage
1378         // ERR_DISK_FULL
1379         errorString = i18n(
1380             "The destination resource does not have sufficient space "
1381             "to record the state of the resource after the execution "
1382             "of this method.");
1383         break;
1384     default:
1385         break;
1386     }
1387 
1388     errorMsg = errorString;
1389     return KIO::WorkerResult::fail(errorCode, errorString);
1390 }
1391 
1392 // HTTP generic error
1393 static int httpGenericError(int responseCode, QString *errorString)
1394 {
1395     Q_ASSERT(errorString);
1396 
1397     int errorCode = 0;
1398     errorString->clear();
1399 
1400     if (responseCode == 204) {
1401         errorCode = KIO::ERR_NO_CONTENT;
1402     }
1403 
1404     if (responseCode >= 400 && responseCode <= 499) {
1405         errorCode = KIO::ERR_DOES_NOT_EXIST;
1406     }
1407 
1408     if (responseCode >= 500 && responseCode <= 599) {
1409         errorCode = KIO::ERR_INTERNAL_SERVER;
1410     }
1411 
1412     return errorCode;
1413 }
1414 
1415 // HTTP DELETE specific errors
1416 static int httpDelError(int responseCode, QString *errorString)
1417 {
1418     Q_ASSERT(errorString);
1419 
1420     int errorCode = 0;
1421     errorString->clear();
1422 
1423     switch (responseCode) {
1424     case 204:
1425         errorCode = KIO::ERR_NO_CONTENT;
1426         break;
1427     default:
1428         break;
1429     }
1430 
1431     if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) {
1432         errorCode = KIO::ERR_WORKER_DEFINED;
1433         *errorString = i18n("The resource cannot be deleted.");
1434     }
1435 
1436     if (responseCode >= 400 && responseCode <= 499) {
1437         errorCode = KIO::ERR_DOES_NOT_EXIST;
1438     }
1439 
1440     if (responseCode >= 500 && responseCode <= 599) {
1441         errorCode = KIO::ERR_INTERNAL_SERVER;
1442     }
1443 
1444     return errorCode;
1445 }
1446 
1447 // HTTP PUT specific errors
1448 static int httpPutError(const QUrl &url, int responseCode, QString *errorString)
1449 {
1450     Q_ASSERT(errorString);
1451 
1452     int errorCode = 0;
1453     const QString action(i18nc("request type", "upload %1", url.toDisplayString()));
1454 
1455     switch (responseCode) {
1456     case 403:
1457     case 405:
1458     case 500: // hack: Apache mod_dav returns this instead of 403 (!)
1459         // 403 Forbidden
1460         // 405 Method Not Allowed
1461         // ERR_ACCESS_DENIED
1462         *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action);
1463         errorCode = KIO::ERR_WORKER_DEFINED;
1464         break;
1465     case 409:
1466         // 409 Conflict
1467         // ERR_ACCESS_DENIED
1468         *errorString = i18n(
1469             "A resource cannot be created at the destination "
1470             "until one or more intermediate collections (folders) "
1471             "have been created.");
1472         errorCode = KIO::ERR_WORKER_DEFINED;
1473         break;
1474     case 423:
1475         // 423 Locked
1476         // ERR_ACCESS_DENIED
1477         *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action);
1478         errorCode = KIO::ERR_WORKER_DEFINED;
1479         break;
1480     case 502:
1481         // 502 Bad Gateway
1482         // ERR_WRITE_ACCESS_DENIED;
1483         *errorString = i18nc("%1: request type",
1484                              "Unable to %1 because the destination server refuses "
1485                              "to accept the file or folder.",
1486                              action);
1487         errorCode = KIO::ERR_WORKER_DEFINED;
1488         break;
1489     case 507:
1490         // 507 Insufficient Storage
1491         // ERR_DISK_FULL
1492         *errorString = i18n(
1493             "The destination resource does not have sufficient space "
1494             "to record the state of the resource after the execution "
1495             "of this method.");
1496         errorCode = KIO::ERR_WORKER_DEFINED;
1497         break;
1498     default:
1499         break;
1500     }
1501 
1502     if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) {
1503         errorCode = KIO::ERR_WORKER_DEFINED;
1504         *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action);
1505     }
1506 
1507     if (responseCode >= 400 && responseCode <= 499) {
1508         errorCode = KIO::ERR_DOES_NOT_EXIST;
1509     }
1510 
1511     if (responseCode >= 500 && responseCode <= 599) {
1512         errorCode = KIO::ERR_INTERNAL_SERVER;
1513     }
1514 
1515     return errorCode;
1516 }
1517 
1518 KIO::WorkerResult HTTPProtocol::sendHttpError(const QUrl &url, KIO::HTTP_METHOD method, const HTTPProtocol::Response &response)
1519 {
1520     QString errorString;
1521     int errorCode = 0;
1522 
1523     if (response.kioCode == KIO::ERR_ACCESS_DENIED) {
1524         return KIO::WorkerResult::fail(response.kioCode, url.toDisplayString());
1525     }
1526 
1527     int responseCode = response.httpCode;
1528 
1529     if (method == KIO::HTTP_PUT) {
1530         errorCode = httpPutError(url, responseCode, &errorString);
1531     } else if (method == KIO::HTTP_DELETE) {
1532         errorCode = httpDelError(responseCode, &errorString);
1533     } else {
1534         errorCode = httpGenericError(responseCode, &errorString);
1535     }
1536 
1537     if (errorCode) {
1538         return KIO::WorkerResult::fail(errorCode, errorString);
1539     }
1540 
1541     return KIO::WorkerResult::pass();
1542 }
1543 
1544 QByteArray HTTPProtocol::methodToString(KIO::HTTP_METHOD method)
1545 {
1546     switch (method) {
1547     case KIO::HTTP_GET:
1548         return "GET";
1549     case KIO::HTTP_PUT:
1550         return "PUT";
1551     case KIO::HTTP_POST:
1552         return "POST";
1553     case KIO::HTTP_HEAD:
1554         return "HEAD";
1555     case KIO::HTTP_DELETE:
1556         return "DELETE";
1557     case KIO::HTTP_OPTIONS:
1558         return "OPTIONS";
1559     case KIO::DAV_PROPFIND:
1560         return "PROPFIND";
1561     case KIO::DAV_PROPPATCH:
1562         return "PROPPATCH";
1563     case KIO::DAV_MKCOL:
1564         return "MKCOL";
1565     case KIO::DAV_COPY:
1566         return "COPY";
1567     case KIO::DAV_MOVE:
1568         return "MOVE";
1569     case KIO::DAV_LOCK:
1570         return "LOCK";
1571     case KIO::DAV_UNLOCK:
1572         return "UNLOCK";
1573     case KIO::DAV_SEARCH:
1574         return "SEARCH";
1575     case KIO::DAV_SUBSCRIBE:
1576         return "SUBSCRIBE";
1577     case KIO::DAV_UNSUBSCRIBE:
1578         return "UNSUBSCRIBE";
1579     case KIO::DAV_POLL:
1580         return "POLL";
1581     case KIO::DAV_NOTIFY:
1582         return "NOTIFY";
1583     case KIO::DAV_REPORT:
1584         return "REPORT";
1585     default:
1586         Q_ASSERT(false);
1587         return QByteArray();
1588     }
1589 }
1590 
1591 #include "http.moc"