File indexing completed on 2024-04-21 15:00:48

0001 /*
0002     This file is part of the KDE project.
0003     SPDX-FileCopyrightText: 2008 Alex Merry <alex.merry @ kdemail.net>
0004     SPDX-FileCopyrightText: 2008-2009 Urs Wolfer <uwolfer @ kde.org>
0005     SPDX-FileCopyrightText: 2009-2012 Dawit Alemayehu <adawit @ kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "accessmanagerreply_p.h"
0011 #include "accessmanager.h"
0012 #include "job.h"
0013 #include "kio_widgets_debug.h"
0014 
0015 #include <kprotocolinfo.h>
0016 #include <kurlauthorized.h>
0017 
0018 #include <QMimeDatabase>
0019 #include <QSslConfiguration>
0020 #include <QtMath>
0021 
0022 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 107)
0023 
0024 namespace KDEPrivate
0025 {
0026 AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op,
0027                                        const QNetworkRequest &request,
0028                                        KIO::SimpleJob *kioJob,
0029                                        bool emitReadyReadOnMetaDataChange,
0030                                        QObject *parent)
0031     : QNetworkReply(parent)
0032     , m_offset(0)
0033     , m_metaDataRead(false)
0034     , m_ignoreContentDisposition(false)
0035     , m_emitReadyReadOnMetaDataChange(emitReadyReadOnMetaDataChange)
0036     , m_kioJob(kioJob)
0037 
0038 {
0039     setRequest(request);
0040     setOpenMode(QIODevice::ReadOnly);
0041     setUrl(request.url());
0042     setOperation(op);
0043     setError(NoError, QString());
0044 
0045     if (!request.sslConfiguration().isNull()) {
0046         setSslConfiguration(request.sslConfiguration());
0047     }
0048 
0049     connect(kioJob, &KJob::percentChanged, this, &AccessManagerReply::slotPercent);
0050 
0051     if (auto *statJob = qobject_cast<KIO::StatJob *>(kioJob)) {
0052         connect(statJob, &KIO::StatJob::redirection, this, &AccessManagerReply::slotRedirection);
0053         connect(statJob, &KJob::result, this, &AccessManagerReply::slotStatResult);
0054     } else if (auto *tJob = qobject_cast<KIO::TransferJob *>(kioJob)) {
0055         connect(tJob, &KJob::result, this, &AccessManagerReply::slotResult);
0056         connect(tJob, &KIO::TransferJob::redirection, this, &AccessManagerReply::slotRedirection);
0057         connect(tJob, &KIO::TransferJob::data, this, &AccessManagerReply::slotData);
0058         connect(tJob, &KIO::TransferJob::mimeTypeFound, this, &AccessManagerReply::slotMimeType);
0059     } else {
0060         connect(kioJob, &KJob::result, this, &AccessManagerReply::slotResult);
0061     }
0062 }
0063 
0064 AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op,
0065                                        const QNetworkRequest &request,
0066                                        const QByteArray &data,
0067                                        const QUrl &url,
0068                                        const KIO::MetaData &metaData,
0069                                        QObject *parent)
0070     : QNetworkReply(parent)
0071     , m_data(data)
0072     , m_offset(0)
0073     , m_ignoreContentDisposition(false)
0074     , m_emitReadyReadOnMetaDataChange(false)
0075 {
0076     setRequest(request);
0077     setOpenMode(QIODevice::ReadOnly);
0078     setUrl((url.isValid() ? url : request.url()));
0079     setOperation(op);
0080     setHeaderFromMetaData(metaData);
0081 
0082     if (!request.sslConfiguration().isNull()) {
0083         setSslConfiguration(request.sslConfiguration());
0084     }
0085 
0086     setError(NoError, QString());
0087     emitFinished(true, Qt::QueuedConnection);
0088 }
0089 
0090 AccessManagerReply::AccessManagerReply(const QNetworkAccessManager::Operation op,
0091                                        const QNetworkRequest &request,
0092                                        QNetworkReply::NetworkError errorCode,
0093                                        const QString &errorMessage,
0094                                        QObject *parent)
0095     : QNetworkReply(parent)
0096     , m_offset(0)
0097 {
0098     setRequest(request);
0099     setOpenMode(QIODevice::ReadOnly);
0100     setUrl(request.url());
0101     setOperation(op);
0102     setError(static_cast<QNetworkReply::NetworkError>(errorCode), errorMessage);
0103     const auto networkError = error();
0104     if (networkError != QNetworkReply::NoError) {
0105         auto occurrFunc = [this, networkError]() {
0106             Q_EMIT errorOccurred(networkError);
0107         };
0108         QMetaObject::invokeMethod(this, occurrFunc, Qt::QueuedConnection);
0109     }
0110 
0111     emitFinished(true, Qt::QueuedConnection);
0112 }
0113 
0114 AccessManagerReply::~AccessManagerReply()
0115 {
0116 }
0117 
0118 void AccessManagerReply::abort()
0119 {
0120     if (m_kioJob) {
0121         m_kioJob.data()->disconnect(this);
0122     }
0123     m_kioJob.clear();
0124     m_data.clear();
0125     m_offset = 0;
0126     m_metaDataRead = false;
0127 }
0128 
0129 qint64 AccessManagerReply::bytesAvailable() const
0130 {
0131     return (QNetworkReply::bytesAvailable() + m_data.length() - m_offset);
0132 }
0133 
0134 qint64 AccessManagerReply::readData(char *data, qint64 maxSize)
0135 {
0136     const qint64 length = qMin(qint64(m_data.length() - m_offset), maxSize);
0137 
0138     if (length <= 0) {
0139         return 0;
0140     }
0141 
0142     memcpy(data, m_data.constData() + m_offset, length);
0143     m_offset += length;
0144 
0145     if (m_data.length() == m_offset) {
0146         m_data.clear();
0147         m_offset = 0;
0148     }
0149 
0150     return length;
0151 }
0152 
0153 bool AccessManagerReply::ignoreContentDisposition(const KIO::MetaData &metaData)
0154 {
0155     if (m_ignoreContentDisposition) {
0156         return true;
0157     }
0158 
0159     if (!metaData.contains(QLatin1String("content-disposition-type"))) {
0160         return true;
0161     }
0162 
0163     bool ok = false;
0164     const int statusCode = attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok);
0165     if (!ok || statusCode < 200 || statusCode > 299) {
0166         return true;
0167     }
0168 
0169     return false;
0170 }
0171 
0172 void AccessManagerReply::setHeaderFromMetaData(const KIO::MetaData &_metaData)
0173 {
0174     if (_metaData.isEmpty()) {
0175         return;
0176     }
0177 
0178     KIO::MetaData metaData(_metaData);
0179 
0180     // Set the encryption attribute and values...
0181     QSslConfiguration sslConfig;
0182     const bool isEncrypted = KIO::Integration::sslConfigFromMetaData(metaData, sslConfig);
0183     setAttribute(QNetworkRequest::ConnectionEncryptedAttribute, isEncrypted);
0184     if (isEncrypted) {
0185         setSslConfiguration(sslConfig);
0186     }
0187 
0188     // Set the raw header information...
0189     const QStringList httpHeaders(metaData.value(QStringLiteral("HTTP-Headers")).split(QLatin1Char('\n'), Qt::SkipEmptyParts));
0190     if (httpHeaders.isEmpty()) {
0191         const auto charSetIt = metaData.constFind(QStringLiteral("charset"));
0192         if (charSetIt != metaData.constEnd()) {
0193             QString mimeType = header(QNetworkRequest::ContentTypeHeader).toString();
0194             mimeType += QLatin1String(" ; charset=") + *charSetIt;
0195             // qDebug() << "changed content-type to" << mimeType;
0196             setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8());
0197         }
0198     } else {
0199         for (const QString &httpHeader : httpHeaders) {
0200             int index = httpHeader.indexOf(QLatin1Char(':'));
0201             // Handle HTTP status line...
0202             if (index == -1) {
0203                 // Except for the status line, all HTTP header must be an nvpair of
0204                 // type "<name>:<value>"
0205                 if (!httpHeader.startsWith(QLatin1String("HTTP/"), Qt::CaseInsensitive)) {
0206                     continue;
0207                 }
0208 
0209                 QStringList statusLineAttrs(httpHeader.split(QLatin1Char(' '), Qt::SkipEmptyParts));
0210                 if (statusLineAttrs.count() > 1) {
0211                     setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusLineAttrs.at(1));
0212                 }
0213 
0214                 if (statusLineAttrs.count() > 2) {
0215                     setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, statusLineAttrs.at(2));
0216                 }
0217 
0218                 continue;
0219             }
0220 
0221             const auto headerName = QStringView(httpHeader).left(index);
0222             // Ignore cookie header since it is handled by the http KIO worker.
0223             if (headerName.startsWith(QLatin1String("set-cookie"), Qt::CaseInsensitive)) {
0224                 continue;
0225             }
0226 
0227             if (headerName.startsWith(QLatin1String("content-disposition"), Qt::CaseInsensitive) && ignoreContentDisposition(metaData)) {
0228                 continue;
0229             }
0230 
0231             QString headerValue = httpHeader.mid(index + 1);
0232             // Without overriding the corrected mime-type sent by kio_http, add
0233             // back the "charset=" portion of the content-type header if present.
0234             if (headerName.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) {
0235                 QString mimeType(header(QNetworkRequest::ContentTypeHeader).toString());
0236 
0237                 if (m_ignoreContentDisposition) {
0238                     // If the server returned application/octet-stream, try to determine the
0239                     // real content type from the disposition filename.
0240                     if (mimeType == QLatin1String("application/octet-stream")) {
0241                         const QString fileName(metaData.value(QStringLiteral("content-disposition-filename")));
0242                         QMimeDatabase db;
0243                         QMimeType mime = db.mimeTypeForFile((fileName.isEmpty() ? url().path() : fileName), QMimeDatabase::MatchExtension);
0244                         mimeType = mime.name();
0245                     }
0246                     metaData.remove(QStringLiteral("content-disposition-type"));
0247                     metaData.remove(QStringLiteral("content-disposition-filename"));
0248                 }
0249 
0250                 if (!headerValue.contains(mimeType, Qt::CaseInsensitive)) {
0251                     index = headerValue.indexOf(QLatin1Char(';'));
0252                     if (index == -1) {
0253                         headerValue = mimeType;
0254                     } else {
0255                         headerValue.replace(0, index, mimeType);
0256                     }
0257                     // qDebug() << "Changed mime-type from" << mimeType << "to" << headerValue;
0258                 }
0259             }
0260             setRawHeader(headerName.trimmed().toUtf8(), headerValue.trimmed().toUtf8());
0261         }
0262     }
0263 
0264     // Set the returned meta data as attribute...
0265     setAttribute(static_cast<QNetworkRequest::Attribute>(KIO::Integration::AccessManager::MetaData), metaData.toVariant());
0266 }
0267 
0268 void AccessManagerReply::setIgnoreContentDisposition(bool on)
0269 {
0270     // qDebug() << on;
0271     m_ignoreContentDisposition = on;
0272 }
0273 
0274 void AccessManagerReply::putOnHold()
0275 {
0276     if (!m_kioJob || isFinished()) {
0277         return;
0278     }
0279 
0280     // qDebug() << m_kioJob << m_data;
0281     m_kioJob.data()->disconnect(this);
0282     m_kioJob.data()->putOnHold();
0283     m_kioJob.clear();
0284 }
0285 
0286 bool AccessManagerReply::isLocalRequest(const QUrl &url)
0287 {
0288     const QString scheme(url.scheme());
0289     return (KProtocolInfo::isKnownProtocol(scheme) && KProtocolInfo::protocolClass(scheme).compare(QStringLiteral(":local"), Qt::CaseInsensitive) == 0);
0290 }
0291 
0292 void AccessManagerReply::readHttpResponseHeaders(KIO::Job *job)
0293 {
0294     if (!job || m_metaDataRead) {
0295         return;
0296     }
0297 
0298     KIO::MetaData metaData(job->metaData());
0299     if (metaData.isEmpty()) {
0300         // Allow handling of local resources such as man pages and file url...
0301         if (isLocalRequest(url())) {
0302             setHeader(QNetworkRequest::ContentLengthHeader, job->totalAmount(KJob::Bytes));
0303             setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QStringLiteral("200"));
0304             Q_EMIT metaDataChanged();
0305         }
0306         return;
0307     }
0308 
0309     setHeaderFromMetaData(metaData);
0310     m_metaDataRead = true;
0311     Q_EMIT metaDataChanged();
0312 }
0313 
0314 int AccessManagerReply::jobError(KJob *kJob)
0315 {
0316     const int errCode = kJob->error();
0317     switch (errCode) {
0318     case 0:
0319         break; // No error;
0320     case KIO::ERR_WORKER_DEFINED:
0321     case KIO::ERR_NO_CONTENT: // Sent by a 204 response is not an error condition.
0322         setError(QNetworkReply::NoError, kJob->errorText());
0323         break;
0324     case KIO::ERR_IS_DIRECTORY:
0325         // This error condition can happen if you click on an ftp link that points
0326         // to a directory instead of a file, e.g. ftp://ftp.kde.org/pub
0327         setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("inode/directory"));
0328         setError(QNetworkReply::NoError, kJob->errorText());
0329         break;
0330     case KIO::ERR_CANNOT_CONNECT:
0331         setError(QNetworkReply::ConnectionRefusedError, kJob->errorText());
0332         break;
0333     case KIO::ERR_UNKNOWN_HOST:
0334         setError(QNetworkReply::HostNotFoundError, kJob->errorText());
0335         break;
0336     case KIO::ERR_SERVER_TIMEOUT:
0337         setError(QNetworkReply::TimeoutError, kJob->errorText());
0338         break;
0339     case KIO::ERR_USER_CANCELED:
0340     case KIO::ERR_ABORTED:
0341         setError(QNetworkReply::OperationCanceledError, kJob->errorText());
0342         break;
0343     case KIO::ERR_UNKNOWN_PROXY_HOST:
0344         setError(QNetworkReply::ProxyNotFoundError, kJob->errorText());
0345         break;
0346     case KIO::ERR_ACCESS_DENIED:
0347         setError(QNetworkReply::ContentAccessDenied, kJob->errorText());
0348         break;
0349     case KIO::ERR_WRITE_ACCESS_DENIED:
0350         setError(QNetworkReply::ContentOperationNotPermittedError, kJob->errorText());
0351         break;
0352     case KIO::ERR_DOES_NOT_EXIST:
0353         setError(QNetworkReply::ContentNotFoundError, kJob->errorText());
0354         break;
0355     case KIO::ERR_CANNOT_AUTHENTICATE:
0356         setError(QNetworkReply::AuthenticationRequiredError, kJob->errorText());
0357         break;
0358     case KIO::ERR_UNSUPPORTED_PROTOCOL:
0359     case KIO::ERR_NO_SOURCE_PROTOCOL:
0360         setError(QNetworkReply::ProtocolUnknownError, kJob->errorText());
0361         break;
0362     case KIO::ERR_CONNECTION_BROKEN:
0363         setError(QNetworkReply::RemoteHostClosedError, kJob->errorText());
0364         break;
0365     case KIO::ERR_UNSUPPORTED_ACTION:
0366         setError(QNetworkReply::ProtocolInvalidOperationError, kJob->errorText());
0367         break;
0368     default:
0369         setError(QNetworkReply::UnknownNetworkError, kJob->errorText());
0370     }
0371 
0372     return errCode;
0373 }
0374 
0375 void AccessManagerReply::slotData(KIO::Job *kioJob, const QByteArray &data)
0376 {
0377     Q_UNUSED(kioJob);
0378     if (data.isEmpty()) {
0379         return;
0380     }
0381 
0382     qint64 newSizeWithOffset = m_data.size() + data.size();
0383     if (newSizeWithOffset <= m_data.capacity()) {
0384         // Already enough space
0385     } else if (newSizeWithOffset - m_offset <= m_data.capacity()) {
0386         // We get enough space with ::remove.
0387         m_data.remove(0, m_offset);
0388         m_offset = 0;
0389     } else {
0390         // We have to resize the array, which implies an expensive memmove.
0391         // Do it ourselves to save m_offset bytes.
0392         QByteArray newData;
0393         // Leave some free space to avoid that every slotData call results in
0394         // a reallocation. qNextPowerOfTwo is what QByteArray does internally.
0395         newData.reserve(qNextPowerOfTwo(newSizeWithOffset - m_offset));
0396         newData.append(m_data.constData() + m_offset, m_data.size() - m_offset);
0397         m_data = newData;
0398         m_offset = 0;
0399     }
0400 
0401     m_data += data;
0402 
0403     Q_EMIT readyRead();
0404 }
0405 
0406 void AccessManagerReply::slotMimeType(KIO::Job *kioJob, const QString &mimeType)
0407 {
0408     // qDebug() << kioJob << mimeType;
0409     setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8());
0410     readHttpResponseHeaders(kioJob);
0411     if (m_emitReadyReadOnMetaDataChange) {
0412         Q_EMIT readyRead();
0413     }
0414 }
0415 
0416 void AccessManagerReply::slotResult(KJob *kJob)
0417 {
0418     const int errcode = jobError(kJob);
0419 
0420     const QUrl redirectUrl = attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0421     if (!redirectUrl.isValid()) {
0422         setAttribute(static_cast<QNetworkRequest::Attribute>(KIO::Integration::AccessManager::KioError), errcode);
0423         if (errcode && errcode != KIO::ERR_NO_CONTENT) {
0424             const auto networkError = error();
0425             Q_EMIT errorOccurred(networkError);
0426         }
0427     }
0428 
0429     // Make sure HTTP response headers are always set.
0430     if (!m_metaDataRead) {
0431         readHttpResponseHeaders(qobject_cast<KIO::Job *>(kJob));
0432     }
0433 
0434     emitFinished(true);
0435 }
0436 
0437 void AccessManagerReply::slotStatResult(KJob *kJob)
0438 {
0439     if (jobError(kJob)) {
0440         const auto networkError = error();
0441         Q_EMIT errorOccurred(networkError);
0442         emitFinished(true);
0443         return;
0444     }
0445 
0446     KIO::StatJob *statJob = qobject_cast<KIO::StatJob *>(kJob);
0447     Q_ASSERT(statJob);
0448 
0449     KIO::UDSEntry entry = statJob->statResult();
0450     QString mimeType = entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE);
0451     if (mimeType.isEmpty() && entry.isDir()) {
0452         mimeType = QStringLiteral("inode/directory");
0453     }
0454 
0455     if (!mimeType.isEmpty()) {
0456         setHeader(QNetworkRequest::ContentTypeHeader, mimeType.toUtf8());
0457     }
0458 
0459     emitFinished(true);
0460 }
0461 
0462 void AccessManagerReply::slotRedirection(KIO::Job *job, const QUrl &u)
0463 {
0464     if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), url(), u)) {
0465         qCWarning(KIO_WIDGETS) << "Redirection from" << url() << "to" << u << "REJECTED by policy!";
0466         setError(QNetworkReply::ContentAccessDenied, u.toString());
0467         const auto networkError = error();
0468         Q_EMIT errorOccurred(networkError);
0469         return;
0470     }
0471     setAttribute(QNetworkRequest::RedirectionTargetAttribute, QUrl(u));
0472     if (job->queryMetaData(QStringLiteral("redirect-to-get")) == QLatin1String("true")) {
0473         setOperation(QNetworkAccessManager::GetOperation);
0474     }
0475 }
0476 
0477 void AccessManagerReply::slotPercent(KJob *job, unsigned long percent)
0478 {
0479     qulonglong bytesTotal = job->totalAmount(KJob::Bytes);
0480     qulonglong bytesProcessed = (bytesTotal * percent) / 100;
0481     if (operation() == QNetworkAccessManager::PutOperation || operation() == QNetworkAccessManager::PostOperation) {
0482         Q_EMIT uploadProgress(bytesProcessed, bytesTotal);
0483         return;
0484     }
0485     Q_EMIT downloadProgress(bytesProcessed, bytesTotal);
0486 }
0487 
0488 void AccessManagerReply::emitFinished(bool state, Qt::ConnectionType type)
0489 {
0490     setFinished(state);
0491     Q_EMIT QMetaObject::invokeMethod(this, &AccessManagerReply::finished, type);
0492 }
0493 
0494 }
0495 
0496 #include "moc_accessmanagerreply_p.cpp"
0497 
0498 #endif