File indexing completed on 2024-05-12 05:22:15

0001 /*
0002  * This file is part of LibKGAPI library
0003  *
0004  * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007  */
0008 
0009 #include "job.h"
0010 #include "account.h"
0011 #include "authjob.h"
0012 #include "debug.h"
0013 #include "job_p.h"
0014 #include "networkaccessmanagerfactory_p.h"
0015 #include "utils.h"
0016 
0017 #include <QCoreApplication>
0018 #include <QFile>
0019 #include <QJsonDocument>
0020 #include <QTextStream>
0021 #include <QUrlQuery>
0022 
0023 using namespace KGAPI2;
0024 
0025 FileLogger *FileLogger::sInstance = nullptr;
0026 
0027 FileLogger::FileLogger()
0028 {
0029     if (!qEnvironmentVariableIsSet("KGAPI_SESSION_LOGFILE")) {
0030         return;
0031     }
0032 
0033     QString filename = QString::fromLocal8Bit(qgetenv("KGAPI_SESSION_LOGFILE")) + QLatin1Char('.') + QString::number(QCoreApplication::applicationPid());
0034     mFile.reset(new QFile(filename));
0035     if (!mFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
0036         qCWarning(KGAPIDebug) << "Failed to open logging file" << filename << ":" << mFile->errorString();
0037         mFile.reset();
0038     }
0039 }
0040 
0041 FileLogger::~FileLogger()
0042 {
0043 }
0044 
0045 FileLogger *FileLogger::self()
0046 {
0047     if (!sInstance) {
0048         sInstance = new FileLogger();
0049     }
0050     return sInstance;
0051 }
0052 
0053 void FileLogger::logRequest(const QNetworkRequest &request, const QByteArray &rawData)
0054 {
0055     if (!mFile) {
0056         return;
0057     }
0058 
0059     QTextStream stream(mFile.data());
0060     stream << "C: " << request.url().toDisplayString() << "\n";
0061     const auto headers = request.rawHeaderList();
0062     for (const auto &header : headers) {
0063         stream << "   " << header << ": " << request.rawHeader(header) << "\n";
0064     }
0065     stream << "   " << rawData << "\n\n";
0066     mFile->flush();
0067 }
0068 
0069 void FileLogger::logReply(const QNetworkReply *reply, const QByteArray &rawData)
0070 {
0071     if (!mFile) {
0072         return;
0073     }
0074 
0075     QTextStream stream(mFile.data());
0076     stream << "S: " << reply->url().toDisplayString() << "\n";
0077     const auto headers = reply->rawHeaderList();
0078     for (const auto &header : headers) {
0079         stream << "   " << header << ": " << reply->rawHeader(header) << "\n";
0080     }
0081     stream << "   " << rawData << "\n\n";
0082     mFile->flush();
0083 }
0084 
0085 Job::Private::Private(Job *parent)
0086     : isRunning(false)
0087     , error(KGAPI2::NoError)
0088     , accessManager(nullptr)
0089     , maxTimeout(0)
0090     , prettyPrint(false)
0091     , q(parent)
0092 {
0093 }
0094 
0095 void Job::Private::init()
0096 {
0097     QTimer::singleShot(0, q, [this]() {
0098         _k_doStart();
0099     });
0100 
0101     accessManager = NetworkAccessManagerFactory::instance()->networkAccessManager(q);
0102     connect(accessManager, &QNetworkAccessManager::finished, q, [this](QNetworkReply *reply) {
0103         _k_replyReceived(reply);
0104     });
0105 
0106     dispatchTimer = new QTimer(q);
0107     connect(dispatchTimer, &QTimer::timeout, q, [this]() {
0108         _k_dispatchTimeout();
0109     });
0110 }
0111 
0112 QString Job::Private::parseErrorMessage(const QByteArray &json)
0113 {
0114     QJsonDocument document = QJsonDocument::fromJson(json);
0115     if (!document.isNull()) {
0116         QVariantMap map = document.toVariant().toMap();
0117         QString message;
0118 
0119         if (map.contains(QStringLiteral("error"))) {
0120             map = map.value(QStringLiteral("error")).toMap();
0121         }
0122 
0123         if (map.contains(QStringLiteral("message"))) {
0124             message.append(map.value(QStringLiteral("message")).toString());
0125         } else {
0126             message = QLatin1StringView(json);
0127         }
0128 
0129         return message;
0130 
0131     } else {
0132         return QLatin1StringView(json);
0133     }
0134 }
0135 
0136 void Job::Private::_k_doStart()
0137 {
0138     isRunning = true;
0139     q->aboutToStart();
0140     q->start();
0141 }
0142 
0143 void Job::Private::_k_doEmitFinished()
0144 {
0145     Q_EMIT q->finished(q);
0146 }
0147 
0148 void Job::Private::_k_replyReceived(QNetworkReply *reply)
0149 {
0150     int replyCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0151     if (replyCode == 0) {
0152         /* Workaround for a bug (??), when QNetworkReply does not report HTTP/1.1 401 Unauthorized
0153          * as an error. */
0154         if (!reply->rawHeaderList().isEmpty()) {
0155             QString status = QLatin1StringView(reply->rawHeaderList().first());
0156             if (status.startsWith(QLatin1StringView("HTTP/1.1 401"))) {
0157                 replyCode = KGAPI2::Unauthorized;
0158             }
0159         }
0160     }
0161 
0162     const QByteArray rawData = reply->readAll();
0163 
0164     qCDebug(KGAPIDebug) << "Received reply from" << reply->url();
0165     qCDebug(KGAPIDebug) << "Status code: " << replyCode;
0166     FileLogger::self()->logReply(reply, rawData);
0167 
0168     switch (replyCode) {
0169     case KGAPI2::NoError:
0170     case KGAPI2::OK: /** << OK status (fetched, updated, removed) */
0171     case KGAPI2::Created: /** << OK status (created) */
0172     case KGAPI2::NoContent: /** << OK status (removed task using Tasks API) */
0173     case KGAPI2::ResumeIncomplete: /** << OK status (partially uploaded a file via resumable upload) */
0174         break;
0175 
0176     case KGAPI2::TemporarilyMovedUseSameMethod: /** << Temporarily moved - Google provides a new URL where to send the request which must use the original
0177                                                    method */
0178     case KGAPI2::TemporarilyMoved: { /** << Temporarily moved - Google provides a new URL where to send the request */
0179         qCDebug(KGAPIDebug) << "Google says: Temporarily moved to " << reply->header(QNetworkRequest::LocationHeader).toUrl();
0180         QNetworkRequest request = currentRequest.request;
0181         request.setUrl(reply->header(QNetworkRequest::LocationHeader).toUrl());
0182         q->enqueueRequest(request, currentRequest.rawData, currentRequest.contentType);
0183         return;
0184     }
0185 
0186     case KGAPI2::BadRequest: /** << Bad request - malformed data, API changed, something went wrong... */
0187         qCWarning(KGAPIDebug) << "Bad request, Google replied '" << rawData << "'";
0188         q->setError(KGAPI2::BadRequest);
0189         q->setErrorString(tr("Bad request."));
0190         q->emitFinished();
0191         return;
0192 
0193     case KGAPI2::Unauthorized: /** << Unauthorized - Access token has expired, request a new token */
0194         qCWarning(KGAPIDebug) << "Unauthorized. Access token has expired or is invalid.";
0195         q->setError(KGAPI2::Unauthorized);
0196         q->setErrorString(tr("Invalid authentication."));
0197         q->emitFinished();
0198         return;
0199 
0200     case KGAPI2::Forbidden: {
0201         qCWarning(KGAPIDebug) << "Requested resource is forbidden.";
0202         const QString msg = parseErrorMessage(rawData);
0203         q->setError(KGAPI2::Forbidden);
0204         q->setErrorString(tr("Requested resource is forbidden.\n\nGoogle replied '%1'").arg(msg));
0205         q->emitFinished();
0206         return;
0207     }
0208 
0209     case KGAPI2::NotFound: {
0210         qCWarning(KGAPIDebug) << "Requested resource does not exist";
0211         const QString msg = parseErrorMessage(rawData);
0212         q->setError(KGAPI2::NotFound);
0213         q->setErrorString(tr("Requested resource does not exist.\n\nGoogle replied '%1'").arg(msg));
0214         // don't emit finished() here, we can get 404 when fetching contact photos or so,
0215         // in that case 404 is not fatal. Let subclass decide whether to terminate or not.
0216         q->handleReply(reply, rawData);
0217 
0218         if (requestQueue.isEmpty()) {
0219             q->emitFinished();
0220         }
0221         return;
0222     }
0223 
0224     case KGAPI2::Conflict: {
0225         qCWarning(KGAPIDebug) << "Conflict. Remote resource is newer then local.";
0226         const QString msg = parseErrorMessage(rawData);
0227         q->setError(KGAPI2::Conflict);
0228         q->setErrorString(tr("Conflict. Remote resource is newer than local.\n\nGoogle replied '%1'").arg(msg));
0229         q->emitFinished();
0230         return;
0231     }
0232 
0233     case KGAPI2::Gone: {
0234         qCWarning(KGAPIDebug) << "Requested resource does not exist anymore.";
0235         const QString msg = parseErrorMessage(rawData);
0236         q->setError(KGAPI2::Gone);
0237         q->setErrorString(tr("Requested resource does not exist anymore.\n\nGoogle replied '%1'").arg(msg));
0238         // don't emit finished() here, 410 means full sync at least for calendar api, let subclass decide.
0239         q->handleReply(reply, rawData);
0240 
0241         if (requestQueue.isEmpty()) {
0242             q->emitFinished();
0243         }
0244         return;
0245     }
0246 
0247     case KGAPI2::InternalError: {
0248         qCWarning(KGAPIDebug) << "Internal server error.";
0249         const QString msg = parseErrorMessage(rawData);
0250         q->setError(KGAPI2::InternalError);
0251         q->setErrorString(tr("Internal server error. Try again later.\n\nGoogle replied '%1'").arg(msg));
0252         q->emitFinished();
0253         return;
0254     }
0255 
0256     case KGAPI2::QuotaExceeded: {
0257         qCWarning(KGAPIDebug) << "User quota exceeded.";
0258 
0259         // Extend the interval (if possible) and enqueue the request again
0260         int interval = dispatchTimer->interval() / 1000;
0261         if (interval == 0) {
0262             interval = 1;
0263         } else if (interval == 1) {
0264             interval = 2;
0265         } else if ((interval > maxTimeout) && (maxTimeout > 0)) {
0266             const QString msg = parseErrorMessage(rawData);
0267             q->setError(KGAPI2::QuotaExceeded);
0268             q->setErrorString(tr("Maximum quota exceeded. Try again later.\n\nGoogle replied '%1'").arg(msg));
0269             q->emitFinished();
0270             return;
0271         } else {
0272             interval = interval ^ 2;
0273         }
0274         qCDebug(KGAPIDebug) << "Increasing dispatch interval to" << interval * 1000 << "msecs";
0275         dispatchTimer->setInterval(interval * 1000);
0276 
0277         const QNetworkRequest request = reply->request();
0278         q->enqueueRequest(request);
0279         if (!dispatchTimer->isActive()) {
0280             dispatchTimer->start();
0281         }
0282         return;
0283     }
0284 
0285     default: { /** Something went wrong, there's nothing we can do about it */
0286         qCWarning(KGAPIDebug) << "Unknown error" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0287         const QString msg = parseErrorMessage(rawData);
0288         q->setError(KGAPI2::UnknownError);
0289         q->setErrorString(tr("Unknown error.\n\nGoogle replied '%1'").arg(msg));
0290         q->emitFinished();
0291         return;
0292     }
0293     }
0294 
0295     q->handleReply(reply, rawData);
0296 
0297     // handleReply has terminated the job, don't continue
0298     if (!q->isRunning()) {
0299         return;
0300     }
0301 
0302     qCDebug(KGAPIDebug) << requestQueue.length() << "requests in requestQueue.";
0303     if (requestQueue.isEmpty()) {
0304         q->emitFinished();
0305         return;
0306     }
0307 
0308     if (!dispatchTimer->isActive()) {
0309         dispatchTimer->start();
0310     }
0311 }
0312 
0313 void Job::Private::_k_dispatchTimeout()
0314 {
0315     if (requestQueue.isEmpty()) {
0316         dispatchTimer->stop();
0317         return;
0318     }
0319 
0320     const Request r = requestQueue.dequeue();
0321     currentRequest = r;
0322 
0323     QNetworkRequest authorizedRequest = r.request;
0324     if (account) {
0325         authorizedRequest.setRawHeader("Authorization", "Bearer " + account->accessToken().toLatin1());
0326     }
0327 
0328     QUrl url = authorizedRequest.url();
0329     QUrlQuery standardParamQuery(url);
0330     if (!fields.isEmpty()) {
0331         standardParamQuery.addQueryItem(Job::StandardParams::Fields, fields.join(QLatin1Char(',')));
0332     }
0333 
0334     if (!standardParamQuery.hasQueryItem(Job::StandardParams::PrettyPrint)) {
0335         standardParamQuery.addQueryItem(Job::StandardParams::PrettyPrint, Utils::bool2Str(prettyPrint));
0336     }
0337 
0338     url.setQuery(standardParamQuery);
0339     authorizedRequest.setUrl(url);
0340 
0341     qCDebug(KGAPIDebug) << q << "Dispatching request to" << r.request.url();
0342     FileLogger::self()->logRequest(authorizedRequest, r.rawData);
0343 
0344     q->dispatchRequest(accessManager, authorizedRequest, r.rawData, r.contentType);
0345 
0346     if (requestQueue.isEmpty()) {
0347         dispatchTimer->stop();
0348     }
0349 }
0350 
0351 /************************* PUBLIC **********************/
0352 
0353 const QString Job::StandardParams::PrettyPrint = QStringLiteral("prettyPrint");
0354 const QString Job::StandardParams::Fields = QStringLiteral("fields");
0355 
0356 Job::Job(QObject *parent)
0357     : QObject(parent)
0358     , d(new Private(this))
0359 {
0360     d->init();
0361 }
0362 
0363 Job::Job(const AccountPtr &account, QObject *parent)
0364     : QObject(parent)
0365     , d(new Private(this))
0366 {
0367     d->account = account;
0368 
0369     d->init();
0370 }
0371 
0372 Job::~Job()
0373 {
0374     delete d;
0375 }
0376 
0377 void Job::setError(Error error)
0378 {
0379     d->error = error;
0380 }
0381 
0382 Error Job::error() const
0383 {
0384     if (isRunning()) {
0385         qCWarning(KGAPIDebug) << "Called error() on running job, returning nothing";
0386         return KGAPI2::NoError;
0387     }
0388 
0389     return d->error;
0390 }
0391 
0392 void Job::setErrorString(const QString &errorString)
0393 {
0394     d->errorString = errorString;
0395 }
0396 
0397 QString Job::errorString() const
0398 {
0399     if (isRunning()) {
0400         qCWarning(KGAPIDebug) << "Called errorString() on running job, returning nothing";
0401         return QString();
0402     }
0403 
0404     return d->errorString;
0405 }
0406 
0407 bool Job::isRunning() const
0408 {
0409     return d->isRunning;
0410 }
0411 
0412 int Job::maxTimeout() const
0413 {
0414     return d->maxTimeout;
0415 }
0416 
0417 void Job::setMaxTimeout(int maxTimeout)
0418 {
0419     if (isRunning()) {
0420         qCWarning(KGAPIDebug) << "Called setMaxTimeout() on running job. Ignoring.";
0421         return;
0422     }
0423 
0424     d->maxTimeout = maxTimeout;
0425 }
0426 
0427 AccountPtr Job::account() const
0428 {
0429     return d->account;
0430 }
0431 
0432 void Job::setAccount(const AccountPtr &account)
0433 {
0434     if (d->isRunning) {
0435         qCWarning(KGAPIDebug) << "Called setAccount() on running job. Ignoring.";
0436         return;
0437     }
0438 
0439     d->account = account;
0440 }
0441 
0442 bool Job::prettyPrint() const
0443 {
0444     return d->prettyPrint;
0445 }
0446 
0447 void Job::setPrettyPrint(bool prettyPrint)
0448 {
0449     if (d->isRunning) {
0450         qCWarning(KGAPIDebug) << "Called setPrettyPrint() on running job. Ignoring.";
0451         return;
0452     }
0453 
0454     d->prettyPrint = prettyPrint;
0455 }
0456 
0457 QStringList Job::fields() const
0458 {
0459     return d->fields;
0460 }
0461 
0462 void Job::setFields(const QStringList &fields)
0463 {
0464     d->fields = fields;
0465 }
0466 
0467 QString Job::buildSubfields(const QString &field, const QStringList &fields)
0468 {
0469     return QStringLiteral("%1(%2)").arg(field, fields.join(QLatin1Char(',')));
0470 }
0471 
0472 void Job::restart()
0473 {
0474     if (d->isRunning) {
0475         qCWarning(KGAPIDebug) << "Running job cannot be restarted.";
0476         return;
0477     }
0478 
0479     QTimer::singleShot(0, this, [this]() {
0480         d->_k_doStart();
0481     });
0482 }
0483 
0484 void Job::emitFinished()
0485 {
0486     aboutToFinish();
0487 
0488     d->isRunning = false;
0489     d->dispatchTimer->stop();
0490     d->requestQueue.clear();
0491 
0492     // Emit in next event loop iteration so that the method caller can finish
0493     // before user is notified
0494     QTimer::singleShot(0, this, [this]() {
0495         d->_k_doEmitFinished();
0496     });
0497 }
0498 
0499 void Job::emitProgress(int processed, int total)
0500 {
0501     Q_EMIT progress(this, processed, total);
0502 }
0503 
0504 void Job::enqueueRequest(const QNetworkRequest &request, const QByteArray &data, const QString &contentType)
0505 {
0506     if (!isRunning()) {
0507         qCDebug(KGAPIDebug) << "Can't enqueue requests when job is not running.";
0508         qCDebug(KGAPIDebug) << "Not enqueueing" << request.url();
0509         return;
0510     }
0511 
0512     qCDebug(KGAPIDebug) << "Queued" << request.url();
0513 
0514     Request r_;
0515     r_.request = request;
0516     r_.rawData = data;
0517     r_.contentType = contentType;
0518 
0519     d->requestQueue.enqueue(r_);
0520 
0521     if (!d->dispatchTimer->isActive()) {
0522         d->dispatchTimer->start();
0523     }
0524 }
0525 
0526 void Job::aboutToFinish()
0527 {
0528 }
0529 
0530 void Job::aboutToStart()
0531 {
0532     d->error = KGAPI2::NoError;
0533     d->errorString.clear();
0534     d->currentRequest.contentType.clear();
0535     d->currentRequest.rawData.clear();
0536     d->currentRequest.request = QNetworkRequest();
0537     d->dispatchTimer->setInterval(0);
0538 }
0539 
0540 #include "moc_job.cpp"