File indexing completed on 2025-03-16 06:51:57
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"