Kgapi

job.cpp
1/*
2 * This file is part of LibKGAPI library
3 *
4 * SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8
9#include "job.h"
10#include "account.h"
11#include "authjob.h"
12#include "debug.h"
13#include "job_p.h"
14#include "networkaccessmanagerfactory_p.h"
15#include "utils.h"
16
17#include <QCoreApplication>
18#include <QFile>
19#include <QJsonDocument>
20#include <QTextStream>
21#include <QUrlQuery>
22
23using namespace KGAPI2;
24
25FileLogger *FileLogger::sInstance = nullptr;
26
27FileLogger::FileLogger()
28{
29 if (!qEnvironmentVariableIsSet("KGAPI_SESSION_LOGFILE")) {
30 return;
31 }
32
33 QString filename = QString::fromLocal8Bit(qgetenv("KGAPI_SESSION_LOGFILE")) + QLatin1Char('.') + QString::number(QCoreApplication::applicationPid());
34 mFile.reset(new QFile(filename));
35 if (!mFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
36 qCWarning(KGAPIDebug) << "Failed to open logging file" << filename << ":" << mFile->errorString();
37 mFile.reset();
38 }
39}
40
41FileLogger::~FileLogger()
42{
43}
44
45FileLogger *FileLogger::self()
46{
47 if (!sInstance) {
48 sInstance = new FileLogger();
49 }
50 return sInstance;
51}
52
53void FileLogger::logRequest(const QNetworkRequest &request, const QByteArray &rawData)
54{
55 if (!mFile) {
56 return;
57 }
58
59 QTextStream stream(mFile.data());
60 stream << "C: " << request.url().toDisplayString() << "\n";
61 const auto headers = request.rawHeaderList();
62 for (const auto &header : headers) {
63 stream << " " << header << ": " << request.rawHeader(header) << "\n";
64 }
65 stream << " " << rawData << "\n\n";
66 mFile->flush();
67}
68
69void FileLogger::logReply(const QNetworkReply *reply, const QByteArray &rawData)
70{
71 if (!mFile) {
72 return;
73 }
74
75 QTextStream stream(mFile.data());
76 stream << "S: " << reply->url().toDisplayString() << "\n";
77 const auto headers = reply->rawHeaderList();
78 for (const auto &header : headers) {
79 stream << " " << header << ": " << reply->rawHeader(header) << "\n";
80 }
81 stream << " " << rawData << "\n\n";
82 mFile->flush();
83}
84
85Job::Private::Private(Job *parent)
86 : isRunning(false)
88 , accessManager(nullptr)
89 , maxTimeout(0)
90 , prettyPrint(false)
91 , q(parent)
92{
93}
94
95void Job::Private::init()
96{
97 QTimer::singleShot(0, q, [this]() {
98 _k_doStart();
99 });
100
101 accessManager = NetworkAccessManagerFactory::instance()->networkAccessManager(q);
102 connect(accessManager, &QNetworkAccessManager::finished, q, [this](QNetworkReply *reply) {
103 _k_replyReceived(reply);
104 });
105
106 dispatchTimer = new QTimer(q);
107 connect(dispatchTimer, &QTimer::timeout, q, [this]() {
108 _k_dispatchTimeout();
109 });
110}
111
112QString Job::Private::parseErrorMessage(const QByteArray &json)
113{
114 QJsonDocument document = QJsonDocument::fromJson(json);
115 if (!document.isNull()) {
116 QVariantMap map = document.toVariant().toMap();
117 QString message;
118
119 if (map.contains(QStringLiteral("error"))) {
120 map = map.value(QStringLiteral("error")).toMap();
121 }
122
123 if (map.contains(QStringLiteral("message"))) {
124 message.append(map.value(QStringLiteral("message")).toString());
125 } else {
126 message = QLatin1StringView(json);
127 }
128
129 return message;
130
131 } else {
132 return QLatin1StringView(json);
133 }
134}
135
136void Job::Private::_k_doStart()
137{
138 isRunning = true;
139 q->aboutToStart();
140 q->start();
141}
142
143void Job::Private::_k_doEmitFinished()
144{
145 Q_EMIT q->finished(q);
146}
147
148void Job::Private::_k_replyReceived(QNetworkReply *reply)
149{
151 if (replyCode == 0) {
152 /* Workaround for a bug (??), when QNetworkReply does not report HTTP/1.1 401 Unauthorized
153 * as an error. */
154 if (!reply->rawHeaderList().isEmpty()) {
156 if (status.startsWith(QLatin1StringView("HTTP/1.1 401"))) {
157 replyCode = KGAPI2::Unauthorized;
158 }
159 }
160 }
161
162 const QByteArray rawData = reply->readAll();
163
164 qCDebug(KGAPIDebug) << "Received reply from" << reply->url();
165 qCDebug(KGAPIDebug) << "Status code: " << replyCode;
166 FileLogger::self()->logReply(reply, rawData);
167
168 switch (replyCode) {
169 case KGAPI2::NoError:
170 case KGAPI2::OK: /** << OK status (fetched, updated, removed) */
171 case KGAPI2::Created: /** << OK status (created) */
172 case KGAPI2::NoContent: /** << OK status (removed task using Tasks API) */
173 case KGAPI2::ResumeIncomplete: /** << OK status (partially uploaded a file via resumable upload) */
174 q->handleReply(reply, rawData);
175 break;
176
177 case KGAPI2::TemporarilyMovedUseSameMethod: /** << Temporarily moved - Google provides a new URL where to send the request which must use the original
178 method */
179 case KGAPI2::TemporarilyMoved: { /** << Temporarily moved - Google provides a new URL where to send the request */
180 qCDebug(KGAPIDebug) << "Google says: Temporarily moved to " << reply->header(QNetworkRequest::LocationHeader).toUrl();
181 QNetworkRequest request = currentRequest.request;
183 q->enqueueRequest(request, currentRequest.rawData, currentRequest.contentType);
184 break;
185 }
186
187 case KGAPI2::BadRequest: /** << Bad request - malformed data, API changed, something went wrong... */
188 if (!q->handleError(replyCode, rawData)) {
189 qCWarning(KGAPIDebug) << "Bad request" << reply->url() << ", Google replied '" << rawData << "'";
190 q->setError(KGAPI2::BadRequest);
191 q->setErrorString(tr("Bad request."));
192 q->emitFinished();
193 return;
194 }
195 break;
196
197 case KGAPI2::Unauthorized: /** << Unauthorized - Access token has expired, request a new token */
198 if (!q->handleError(replyCode, rawData)) {
199 qCWarning(KGAPIDebug) << "Unauthorized. Access token has expired or is invalid.";
200 q->setError(KGAPI2::Unauthorized);
201 q->setErrorString(tr("Invalid authentication."));
202 q->emitFinished();
203 return;
204 }
205 break;
206
208 if (!q->handleError(replyCode, rawData)) {
209 qCWarning(KGAPIDebug) << "Requested resource is forbidden.";
210 const QString msg = parseErrorMessage(rawData);
211 q->setError(KGAPI2::Forbidden);
212 q->setErrorString(tr("Requested resource is forbidden.\n\nGoogle replied '%1'").arg(msg));
213 q->emitFinished();
214 return;
215 }
216 break;
217
218 case KGAPI2::NotFound:
219 if (!q->handleError(replyCode, rawData)) {
220 qCWarning(KGAPIDebug) << "Requested resource does not exist";
221 const QString msg = parseErrorMessage(rawData);
222 q->setError(KGAPI2::NotFound);
223 q->setErrorString(tr("Requested resource does not exist.\n\nGoogle replied '%1'").arg(msg));
224 q->emitFinished();
225 return;
226 }
227 break;
228
229 case KGAPI2::Conflict:
230 if (!q->handleError(replyCode, rawData)) {
231 qCWarning(KGAPIDebug) << "Conflict. Remote resource is newer then local.";
232 const QString msg = parseErrorMessage(rawData);
233 q->setError(KGAPI2::Conflict);
234 q->setErrorString(tr("Conflict. Remote resource is newer than local.\n\nGoogle replied '%1'").arg(msg));
235 q->emitFinished();
236 return;
237 }
238 break;
239
240 case KGAPI2::Gone:
241 if (!q->handleError(replyCode, rawData)) {
242 qCWarning(KGAPIDebug) << "Requested resource does not exist anymore.";
243 const QString msg = parseErrorMessage(rawData);
244 q->setError(KGAPI2::Gone);
245 q->setErrorString(tr("Requested resource does not exist anymore.\n\nGoogle replied '%1'").arg(msg));
246 q->emitFinished();
247 return;
248 }
249 break;
250
252 if (!q->handleError(replyCode, rawData)) {
253 qCWarning(KGAPIDebug) << "Internal server error.";
254 const QString msg = parseErrorMessage(rawData);
255 q->setError(KGAPI2::InternalError);
256 q->setErrorString(tr("Internal server error. Try again later.\n\nGoogle replied '%1'").arg(msg));
257 q->emitFinished();
258 return;
259 }
260 break;
261
263 if (!q->handleError(replyCode, rawData)) {
264 qCWarning(KGAPIDebug) << "User quota exceeded.";
265
266 // Extend the interval (if possible) and enqueue the request again
267 int interval = dispatchTimer->interval() / 1000;
268 if (interval == 0) {
269 interval = 1;
270 } else if (interval == 1) {
271 interval = 2;
272 } else if ((interval > maxTimeout) && (maxTimeout > 0)) {
273 const QString msg = parseErrorMessage(rawData);
274 q->setError(KGAPI2::QuotaExceeded);
275 q->setErrorString(tr("Maximum quota exceeded. Try again later.\n\nGoogle replied '%1'").arg(msg));
276 q->emitFinished();
277 return;
278 } else {
279 interval = interval ^ 2;
280 }
281 qCDebug(KGAPIDebug) << "Increasing dispatch interval to" << interval * 1000 << "msecs";
282 dispatchTimer->setInterval(interval * 1000);
283
284 const QNetworkRequest request = reply->request();
285 q->enqueueRequest(request);
286 if (!dispatchTimer->isActive()) {
287 dispatchTimer->start();
288 }
289 return;
290 }
291 break;
292 }
293
294 default: /** Something went wrong, there's nothing we can do about it */
295 if (!q->handleError(replyCode, rawData)) {
296 qCWarning(KGAPIDebug) << "Unknown error" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
297 const QString msg = parseErrorMessage(rawData);
298 q->setError(KGAPI2::UnknownError);
299 q->setErrorString(tr("Unknown error.\n\nGoogle replied '%1'").arg(msg));
300 q->emitFinished();
301 return;
302 }
303 break;
304 }
305
306 // handleReply has terminated the job, don't continue
307 if (!q->isRunning()) {
308 return;
309 }
310
311 qCDebug(KGAPIDebug) << requestQueue.length() << "requests in requestQueue.";
312 if (requestQueue.isEmpty()) {
313 q->emitFinished();
314 return;
315 }
316
317 if (!dispatchTimer->isActive()) {
318 dispatchTimer->start();
319 }
320}
321
322void Job::Private::_k_dispatchTimeout()
323{
324 if (requestQueue.isEmpty()) {
325 dispatchTimer->stop();
326 return;
327 }
328
329 const Request r = requestQueue.dequeue();
330 currentRequest = r;
331
332 QNetworkRequest authorizedRequest = r.request;
333 if (account) {
334 authorizedRequest.setRawHeader("Authorization", "Bearer " + account->accessToken().toLatin1());
335 }
336
337 QUrl url = authorizedRequest.url();
338 QUrlQuery standardParamQuery(url);
339 if (!fields.isEmpty()) {
340 standardParamQuery.addQueryItem(Job::StandardParams::Fields, fields.join(QLatin1Char(',')));
341 }
342
343 if (!standardParamQuery.hasQueryItem(Job::StandardParams::PrettyPrint)) {
344 standardParamQuery.addQueryItem(Job::StandardParams::PrettyPrint, Utils::bool2Str(prettyPrint));
345 }
346
347 url.setQuery(standardParamQuery);
348 authorizedRequest.setUrl(url);
349
350 qCDebug(KGAPIDebug) << q << "Dispatching request to" << r.request.url();
351 FileLogger::self()->logRequest(authorizedRequest, r.rawData);
352
353 q->dispatchRequest(accessManager, authorizedRequest, r.rawData, r.contentType);
354
355 if (requestQueue.isEmpty()) {
356 dispatchTimer->stop();
357 }
358}
359
360/************************* PUBLIC **********************/
361
362const QString Job::StandardParams::PrettyPrint = QStringLiteral("prettyPrint");
363const QString Job::StandardParams::Fields = QStringLiteral("fields");
364
366 : QObject(parent)
367 , d(new Private(this))
368{
369 d->init();
370}
371
372Job::Job(const AccountPtr &account, QObject *parent)
373 : QObject(parent)
374 , d(new Private(this))
375{
376 d->account = account;
377
378 d->init();
379}
380
382{
383 delete d;
384}
385
387{
388 d->error = error;
389}
390
392{
393 if (isRunning()) {
394 qCWarning(KGAPIDebug) << "Called error() on running job, returning nothing";
395 return KGAPI2::NoError;
396 }
397
398 return d->error;
399}
400
401void Job::setErrorString(const QString &errorString)
402{
403 d->errorString = errorString;
404}
405
407{
408 if (isRunning()) {
409 qCWarning(KGAPIDebug) << "Called errorString() on running job, returning nothing";
410 return QString();
411 }
412
413 return d->errorString;
414}
415
416bool Job::isRunning() const
417{
418 return d->isRunning;
419}
420
422{
423 return d->maxTimeout;
424}
425
426void Job::setMaxTimeout(int maxTimeout)
427{
428 if (isRunning()) {
429 qCWarning(KGAPIDebug) << "Called setMaxTimeout() on running job. Ignoring.";
430 return;
431 }
432
433 d->maxTimeout = maxTimeout;
434}
435
437{
438 return d->account;
439}
440
441void Job::setAccount(const AccountPtr &account)
442{
443 if (d->isRunning) {
444 qCWarning(KGAPIDebug) << "Called setAccount() on running job. Ignoring.";
445 return;
446 }
447
448 d->account = account;
449}
450
452{
453 return d->prettyPrint;
454}
455
456void Job::setPrettyPrint(bool prettyPrint)
457{
458 if (d->isRunning) {
459 qCWarning(KGAPIDebug) << "Called setPrettyPrint() on running job. Ignoring.";
460 return;
461 }
462
463 d->prettyPrint = prettyPrint;
464}
465
467{
468 return d->fields;
469}
470
471void Job::setFields(const QStringList &fields)
472{
473 d->fields = fields;
474}
475
476QString Job::buildSubfields(const QString &field, const QStringList &fields)
477{
478 return QStringLiteral("%1(%2)").arg(field, fields.join(QLatin1Char(',')));
479}
480
482{
483 if (d->isRunning) {
484 qCWarning(KGAPIDebug) << "Running job cannot be restarted.";
485 return;
486 }
487
488 QTimer::singleShot(0, this, [this]() {
489 d->_k_doStart();
490 });
491}
492
494{
496
497 d->isRunning = false;
498 d->dispatchTimer->stop();
499 d->requestQueue.clear();
500
501 // Emit in next event loop iteration so that the method caller can finish
502 // before user is notified
503 QTimer::singleShot(0, this, [this]() {
504 d->_k_doEmitFinished();
505 });
506}
507
508void Job::emitProgress(int processed, int total)
509{
510 Q_EMIT progress(this, processed, total);
511}
512
513void Job::enqueueRequest(const QNetworkRequest &request, const QByteArray &data, const QString &contentType)
514{
515 if (!isRunning()) {
516 qCDebug(KGAPIDebug) << "Can't enqueue requests when job is not running.";
517 qCDebug(KGAPIDebug) << "Not enqueueing" << request.url();
518 return;
519 }
520
521 qCDebug(KGAPIDebug) << "Queued" << request.url();
522
523 Request r_;
524 r_.request = request;
525 r_.rawData = data;
526 r_.contentType = contentType;
527
528 d->requestQueue.enqueue(r_);
529
530 if (!d->dispatchTimer->isActive()) {
531 d->dispatchTimer->start();
532 }
533}
534
536{
537}
538
540{
541 d->error = KGAPI2::NoError;
542 d->errorString.clear();
543 d->currentRequest.contentType.clear();
544 d->currentRequest.rawData.clear();
545 d->currentRequest.request = QNetworkRequest();
546 d->dispatchTimer->setInterval(0);
547}
548
549bool Job::handleError(int errorCode, const QByteArray &rawData)
550{
551 Q_UNUSED(errorCode)
552 Q_UNUSED(rawData)
553
554 return false;
555}
556
557#include "moc_job.cpp"
Abstract base class for all jobs in LibKGAPI.
Definition job.h:41
void progress(KGAPI2::Job *job, int processed, int total)
Emitted when a job progress changes.
Job(QObject *parent=nullptr)
Constructor for jobs that don't require authentication.
Definition job.cpp:365
virtual void emitProgress(int processed, int total)
Emit progress() signal.
Definition job.cpp:508
KGAPI2::Error error() const
Error code.
Definition job.cpp:391
virtual void aboutToFinish()
This method is invoked right before finished() is emitted.
Definition job.cpp:535
virtual bool handleError(int statusCode, const QByteArray &rawData)
Called when an error occurs.
Definition job.cpp:549
void setMaxTimeout(int maxTimeout)
Set maximum quota timeout.
Definition job.cpp:426
void setErrorString(const QString &errorString)
Set job error description to errorString.
Definition job.cpp:401
void setAccount(const AccountPtr &account)
Set account to be used to authenticate requests.
Definition job.cpp:441
AccountPtr account() const
Returns account used to authenticate requests.
Definition job.cpp:436
QStringList fields() const
Returns fields selector.
Definition job.cpp:466
int maxTimeout
Maximum interval between requests.
Definition job.h:56
void setFields(const QStringList &fields)
Set subset of fields to include in the response.
Definition job.cpp:471
virtual void aboutToStart()
This method is invoked right before Job::start() is called.
Definition job.cpp:539
QString errorString() const
Error string.
Definition job.cpp:406
virtual void emitFinished()
Emits Job::finished() signal.
Definition job.cpp:493
void setError(KGAPI2::Error error)
Set job error to error.
Definition job.cpp:386
void setPrettyPrint(bool prettyPrint)
Sets whether response will have indentations and line breaks.
Definition job.cpp:456
virtual void enqueueRequest(const QNetworkRequest &request, const QByteArray &data=QByteArray(), const QString &contentType=QString())
Enqueues request in dispatcher queue.
Definition job.cpp:513
~Job() override
Destructor.
Definition job.cpp:381
bool prettyPrint() const
Returns prettyPrint query parameter.
Definition job.cpp:451
bool isRunning
Whether the job is running.
Definition job.h:67
void restart()
Restarts this job.
Definition job.cpp:481
Q_SCRIPTABLE CaptureState status()
char * toString(const EngineQuery &query)
A job to fetch a single map tile described by a StaticMapUrl.
Definition blog.h:16
Error
Job error codes.
Definition types.h:176
@ ResumeIncomplete
Drive Api returns 308 when accepting a partial file upload.
Definition types.h:193
@ Created
Create request successfully executed.
Definition types.h:191
@ Conflict
Object on the remote site differs from the submitted one.
Definition types.h:201
@ Unauthorized
Invalid or expired token. See KGAPI2::Account::refreshTokens().
Definition types.h:198
@ Gone
The requested data does not exist anymore on the remote site.
Definition types.h:202
@ UnknownError
LibKGAPI error - a general unidentified error.
Definition types.h:179
@ InternalError
An unexpected error occurred on the Google service.
Definition types.h:203
@ QuotaExceeded
User quota has been exceeded, the request should be sent again later.
Definition types.h:204
@ Forbidden
The requested data is not accessible to this account.
Definition types.h:199
@ BadRequest
Invalid (malformed) request.
Definition types.h:197
@ NotFound
Requested object was not found on the remote side.
Definition types.h:200
@ OK
Request successfully executed.
Definition types.h:190
@ TemporarilyMoved
The object is located on a different URL provided in reply.
Definition types.h:194
@ NoContent
Tasks API returns 204 when task is successfully removed.
Definition types.h:192
@ TemporarilyMovedUseSameMethod
The object is located at a different URL provided in the reply. The same request method must be used.
Definition types.h:196
@ NoError
LibKGAPI error - no error.
Definition types.h:178
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
qint64 applicationPid()
QByteArray readAll()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isNull() const const
QVariant toVariant() const const
T & first()
bool isEmpty() const const
void finished(QNetworkReply *reply)
QVariant attribute(QNetworkRequest::Attribute code) const const
QVariant header(QNetworkRequest::KnownHeaders header) const const
QByteArray rawHeader(const QByteArray &headerName) const const
QList< QByteArray > rawHeaderList() const const
QNetworkRequest request() const const
QUrl url() const const
QByteArray rawHeader(const QByteArray &headerName) const const
QList< QByteArray > rawHeaderList() const const
void setRawHeader(const QByteArray &headerName, const QByteArray &headerValue)
void setUrl(const QUrl &url)
QUrl url() const const
Q_EMITQ_EMIT
QString & append(QChar ch)
QString arg(Args &&... args) const const
QString fromLocal8Bit(QByteArrayView str)
QString number(double n, char format, int precision)
QString join(QChar separator) const const
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
void setQuery(const QString &query, ParsingMode mode)
QString toDisplayString(FormattingOptions options) const const
int toInt(bool *ok) const const
QMap< QString, QVariant > toMap() const const
QUrl toUrl() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:11:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.