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