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"