1 /******************************************************************************
2 * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include "basejob.h"
20
21 #include "connectiondata.h"
22 #include "util.h"
23
24 #include <QtCore/QJsonObject>
25 #include <QtCore/QRegularExpression>
26 #include <QtCore/QTimer>
27 #include <QtCore/QStringBuilder>
28 #include <QtNetwork/QNetworkAccessManager>
29 #include <QtNetwork/QNetworkReply>
30 #include <QtNetwork/QNetworkRequest>
31
32 #include <array>
33
34 using namespace Quotient;
35 using std::chrono::seconds, std::chrono::milliseconds;
36 using namespace std::chrono_literals;
37
38 struct NetworkReplyDeleter : public QScopedPointerDeleteLater {
cleanupNetworkReplyDeleter39 static inline void cleanup(QNetworkReply* reply)
40 {
41 if (reply && reply->isRunning())
42 reply->abort();
43 QScopedPointerDeleteLater::cleanup(reply);
44 }
45 };
46
47 template <typename... Ts>
make_array(Ts &&...items)48 constexpr auto make_array(Ts&&... items)
49 {
50 return std::array<std::common_type_t<Ts...>, sizeof...(Ts)>({items...});
51 }
52
53 class BaseJob::Private {
54 public:
55 struct JobTimeoutConfig {
56 seconds jobTimeout;
57 seconds nextRetryInterval;
58 };
59
60 // Using an idiom from clang-tidy:
61 // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html
Private(HttpVerb v,QString endpoint,const QUrlQuery & q,Data && data,bool nt)62 Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data,
63 bool nt)
64 : verb(v)
65 , apiEndpoint(std::move(endpoint))
66 , requestQuery(q)
67 , requestData(std::move(data))
68 , needsToken(nt)
69 {
70 timer.setSingleShot(true);
71 retryTimer.setSingleShot(true);
72 }
73
74 void sendRequest();
75
76 ConnectionData* connection = nullptr;
77
78 // Contents for the network request
79 HttpVerb verb;
80 QString apiEndpoint;
81 QHash<QByteArray, QByteArray> requestHeaders;
82 QUrlQuery requestQuery;
83 Data requestData;
84 bool needsToken;
85
86 bool inBackground = false;
87
88 // There's no use of QMimeType here because we don't want to match
89 // content types against the known MIME type hierarchy; and at the same
90 // type QMimeType is of little help with MIME type globs (`text/*` etc.)
91 QByteArrayList expectedContentTypes { "application/json" };
92
93 QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
94 Status status = Unprepared;
95 QByteArray rawResponse;
96 QUrl errorUrl; //< May contain a URL to help with some errors
97
98 LoggingCategory logCat = JOBS;
99
100 QTimer timer;
101 QTimer retryTimer;
102
103 static constexpr std::array<const JobTimeoutConfig, 3> errorStrategy {
104 { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }
105 };
106 int maxRetries = int(errorStrategy.size());
107 int retriesTaken = 0;
108
getCurrentTimeoutConfig() const109 [[nodiscard]] const JobTimeoutConfig& getCurrentTimeoutConfig() const
110 {
111 return errorStrategy[std::min(size_t(retriesTaken),
112 errorStrategy.size() - 1)];
113 }
114
dumpRequest() const115 [[nodiscard]] QString dumpRequest() const
116 {
117 // FIXME: use std::array {} when Apple stdlib gets deduction guides for it
118 static const auto verbs =
119 make_array(QStringLiteral("GET"), QStringLiteral("PUT"),
120 QStringLiteral("POST"), QStringLiteral("DELETE"));
121 const auto verbWord = verbs.at(size_t(verb));
122 return verbWord % ' '
123 % (reply ? reply->url().toString(QUrl::RemoveQuery)
124 : makeRequestUrl(connection->baseUrl(), apiEndpoint)
125 .toString());
126 }
127 };
128
BaseJob(HttpVerb verb,const QString & name,const QString & endpoint,bool needsToken)129 BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
130 bool needsToken)
131 : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken)
132 {}
133
BaseJob(HttpVerb verb,const QString & name,const QString & endpoint,const Query & query,Data && data,bool needsToken)134 BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
135 const Query& query, Data&& data, bool needsToken)
136 : d(new Private(verb, endpoint, query, std::move(data), needsToken))
137 {
138 setObjectName(name);
139 connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
140 connect(&d->retryTimer, &QTimer::timeout, this, [this] {
141 d->connection->submit(this);
142 });
143 }
144
~BaseJob()145 BaseJob::~BaseJob()
146 {
147 stop();
148 d->retryTimer.stop(); // See #398
149 qCDebug(d->logCat) << this << "destroyed";
150 }
151
requestUrl() const152 QUrl BaseJob::requestUrl() const
153 {
154 return d->reply ? d->reply->url() : QUrl();
155 }
156
isBackground() const157 bool BaseJob::isBackground() const
158 {
159 return d->inBackground;
160 }
161
apiEndpoint() const162 const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; }
163
setApiEndpoint(const QString & apiEndpoint)164 void BaseJob::setApiEndpoint(const QString& apiEndpoint)
165 {
166 d->apiEndpoint = apiEndpoint;
167 }
168
requestHeaders() const169 const BaseJob::headers_t& BaseJob::requestHeaders() const
170 {
171 return d->requestHeaders;
172 }
173
setRequestHeader(const headers_t::key_type & headerName,const headers_t::mapped_type & headerValue)174 void BaseJob::setRequestHeader(const headers_t::key_type& headerName,
175 const headers_t::mapped_type& headerValue)
176 {
177 d->requestHeaders[headerName] = headerValue;
178 }
179
setRequestHeaders(const BaseJob::headers_t & headers)180 void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers)
181 {
182 d->requestHeaders = headers;
183 }
184
query() const185 const QUrlQuery& BaseJob::query() const { return d->requestQuery; }
186
setRequestQuery(const QUrlQuery & query)187 void BaseJob::setRequestQuery(const QUrlQuery& query)
188 {
189 d->requestQuery = query;
190 }
191
requestData() const192 const BaseJob::Data& BaseJob::requestData() const { return d->requestData; }
193
setRequestData(Data && data)194 void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); }
195
expectedContentTypes() const196 const QByteArrayList& BaseJob::expectedContentTypes() const
197 {
198 return d->expectedContentTypes;
199 }
200
addExpectedContentType(const QByteArray & contentType)201 void BaseJob::addExpectedContentType(const QByteArray& contentType)
202 {
203 d->expectedContentTypes << contentType;
204 }
205
setExpectedContentTypes(const QByteArrayList & contentTypes)206 void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
207 {
208 d->expectedContentTypes = contentTypes;
209 }
210
makeRequestUrl(QUrl baseUrl,const QString & path,const QUrlQuery & query)211 QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path,
212 const QUrlQuery& query)
213 {
214 auto pathBase = baseUrl.path();
215 if (!pathBase.endsWith('/') && !path.startsWith('/'))
216 pathBase.push_back('/');
217
218 baseUrl.setPath(pathBase + path, QUrl::TolerantMode);
219 baseUrl.setQuery(query);
220 return baseUrl;
221 }
222
sendRequest()223 void BaseJob::Private::sendRequest()
224 {
225 QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint,
226 requestQuery) };
227 if (!requestHeaders.contains("Content-Type"))
228 req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
229 if (needsToken)
230 req.setRawHeader("Authorization",
231 QByteArray("Bearer ") + connection->accessToken());
232 req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground);
233 req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
234 req.setMaximumRedirectsAllowed(10);
235 // Pipelining doesn't fly quite well with SSL, occasionally crashing at
236 // what seems like an attempt to write to a closed channel.
237 // req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);
238 req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
239 Q_ASSERT(req.url().isValid());
240 for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it)
241 req.setRawHeader(it.key(), it.value());
242
243 switch (verb) {
244 case HttpVerb::Get:
245 reply.reset(connection->nam()->get(req));
246 break;
247 case HttpVerb::Post:
248 reply.reset(connection->nam()->post(req, requestData.source()));
249 break;
250 case HttpVerb::Put:
251 reply.reset(connection->nam()->put(req, requestData.source()));
252 break;
253 case HttpVerb::Delete:
254 reply.reset(connection->nam()->deleteResource(req));
255 break;
256 }
257 }
258
doPrepare()259 void BaseJob::doPrepare() {}
260
onSentRequest(QNetworkReply *)261 void BaseJob::onSentRequest(QNetworkReply*) {}
262
beforeAbandon(QNetworkReply *)263 void BaseJob::beforeAbandon(QNetworkReply*) {}
264
initiate(ConnectionData * connData,bool inBackground)265 void BaseJob::initiate(ConnectionData* connData, bool inBackground)
266 {
267 if (connData && connData->baseUrl().isValid()) {
268 d->inBackground = inBackground;
269 d->connection = connData;
270 doPrepare();
271
272 if ((d->verb == HttpVerb::Post || d->verb == HttpVerb::Put)
273 && d->requestData.source() && !d->requestData.source()->isReadable()) {
274 setStatus(FileError, "Request data not ready");
275 }
276 Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this
277 if (status().code == Unprepared) {
278 d->connection->submit(this);
279 return;
280 }
281 qCWarning(d->logCat).noquote()
282 << "Request failed preparation and won't be sent:"
283 << d->dumpRequest();
284 } else {
285 qCCritical(d->logCat)
286 << "Developers, ensure the Connection is valid before using it";
287 Q_ASSERT(false);
288 setStatus(IncorrectRequestError, tr("Invalid server connection"));
289 }
290 // The status is no good, finalise
291 QTimer::singleShot(0, this, &BaseJob::finishJob);
292 }
293
sendRequest()294 void BaseJob::sendRequest()
295 {
296 if (status().code == Abandoned)
297 return;
298 Q_ASSERT(d->connection && status().code == Pending);
299 qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest();
300 d->needsToken |= d->connection->needsToken(objectName());
301 emit aboutToSendRequest();
302 d->sendRequest();
303 Q_ASSERT(d->reply);
304 connect(d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply);
305 if (d->reply->isRunning()) {
306 connect(d->reply.data(), &QNetworkReply::metaDataChanged, this,
307 &BaseJob::checkReply);
308 connect(d->reply.data(), &QNetworkReply::uploadProgress, this,
309 &BaseJob::uploadProgress);
310 connect(d->reply.data(), &QNetworkReply::downloadProgress, this,
311 &BaseJob::downloadProgress);
312 d->timer.start(getCurrentTimeout());
313 qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest();
314 onSentRequest(d->reply.data());
315 emit sentRequest();
316 } else
317 qCCritical(d->logCat).noquote()
318 << "Request could not start:" << d->dumpRequest();
319 }
320
checkReply()321 void BaseJob::checkReply() { setStatus(doCheckReply(d->reply.data())); }
322
gotReply()323 void BaseJob::gotReply()
324 {
325 checkReply();
326 if (status().good())
327 setStatus(parseReply(d->reply.data()));
328 else {
329 d->rawResponse = d->reply->readAll();
330 const auto jsonBody = d->reply->rawHeader("Content-Type")
331 == "application/json";
332 qCDebug(d->logCat).noquote()
333 << "Error body (truncated if long):" << d->rawResponse.left(500);
334 if (jsonBody)
335 setStatus(
336 parseError(d->reply.data(),
337 QJsonDocument::fromJson(d->rawResponse).object()));
338 }
339 finishJob();
340 }
341
checkContentType(const QByteArray & type,const QByteArrayList & patterns)342 bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
343 {
344 if (patterns.isEmpty())
345 return true;
346
347 // ignore possible appendixes of the content type
348 const auto ctype = type.split(';').front();
349
350 for (const auto& pattern: patterns) {
351 if (pattern.startsWith('*') || ctype == pattern) // Fast lane
352 return true;
353
354 auto patternParts = pattern.split('/');
355 Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__,
356 "BaseJob: Expected content type should have up to two"
357 " /-separated parts; violating pattern: "
358 + pattern);
359
360 if (ctype.split('/').front() == patternParts.front()
361 && patternParts.back() == "*")
362 return true; // Exact match already went on fast lane
363 }
364
365 return false;
366 }
367
fromHttpCode(int httpCode)368 BaseJob::StatusCode BaseJob::Status::fromHttpCode(int httpCode)
369 {
370 if (httpCode / 10 == 41) // 41x errors
371 return httpCode == 410 ? IncorrectRequestError : NotFoundError;
372 switch (httpCode) {
373 case 401:
374 return Unauthorised;
375 // clang-format off
376 case 403: case 407: // clang-format on
377 return ContentAccessError;
378 case 404:
379 return NotFoundError;
380 // clang-format off
381 case 400: case 405: case 406: case 426: case 428: case 505: // clang-format on
382 case 494: // Unofficial nginx "Request header too large"
383 case 497: // Unofficial nginx "HTTP request sent to HTTPS port"
384 return IncorrectRequestError;
385 case 429:
386 return TooManyRequestsError;
387 case 501: case 510:
388 return RequestNotImplementedError;
389 case 511:
390 return NetworkAuthRequiredError;
391 default:
392 return NetworkError;
393 }
394 }
395
dumpToLog(QDebug dbg) const396 QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
397 {
398 QDebugStateSaver _s(dbg);
399 dbg.noquote().nospace();
400 if (auto* const k = QMetaEnum::fromType<StatusCode>().valueToKey(code)) {
401 const QByteArray b = k;
402 dbg << b.mid(b.lastIndexOf(':'));
403 } else
404 dbg << code;
405 return dbg << ": " << message;
406
407 }
408
doCheckReply(QNetworkReply * reply) const409 BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
410 {
411 // QNetworkReply error codes seem to be flawed when it comes to HTTP;
412 // see, e.g., https://github.com/quotient-im/libQuotient/issues/200
413 // so check genuine HTTP codes. The below processing is based on
414 // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
415 const auto httpCodeHeader =
416 reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
417 if (!httpCodeHeader.isValid()) {
418 qCWarning(d->logCat).noquote()
419 << "No valid HTTP headers from" << d->dumpRequest();
420 return { NetworkError, reply->errorString() };
421 }
422
423 const auto httpCode = httpCodeHeader.toInt();
424 if (httpCode / 100 == 2) // 2xx
425 {
426 if (reply->isFinished())
427 qCInfo(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest();
428 if (!checkContentType(reply->rawHeader("Content-Type"),
429 d->expectedContentTypes))
430 return { UnexpectedResponseTypeWarning,
431 "Unexpected content type of the response" };
432 return NoError;
433 }
434 if (reply->isFinished())
435 qCWarning(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest();
436
437 auto message = reply->errorString();
438 if (message.isEmpty())
439 message = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute)
440 .toString();
441
442 return Status::fromHttpCode(httpCode, message);
443 }
444
parseReply(QNetworkReply * reply)445 BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
446 {
447 d->rawResponse = reply->readAll();
448 QJsonParseError error { 0, QJsonParseError::MissingObject };
449 const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
450 if (error.error == QJsonParseError::NoError)
451 return parseJson(json);
452
453 return { IncorrectResponseError, error.errorString() };
454 }
455
parseJson(const QJsonDocument &)456 BaseJob::Status BaseJob::parseJson(const QJsonDocument&) { return Success; }
457
parseError(QNetworkReply *,const QJsonObject & errorJson)458 BaseJob::Status BaseJob::parseError(QNetworkReply* /*reply*/,
459 const QJsonObject& errorJson)
460 {
461 const auto errCode = errorJson.value("errcode"_ls).toString();
462 if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
463 QString msg = tr("Too many requests");
464 int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1);
465 if (retryAfterMs >= 0)
466 msg += tr(", next retry advised after %1 ms").arg(retryAfterMs);
467 else // We still have to figure some reasonable interval
468 retryAfterMs = getNextRetryMs();
469
470 d->connection->limitRate(milliseconds(retryAfterMs));
471
472 return { TooManyRequestsError, msg };
473 }
474 if (errCode == "M_CONSENT_NOT_GIVEN") {
475 d->errorUrl = errorJson.value("consent_uri"_ls).toString();
476 return { UserConsentRequiredError };
477 }
478 if (errCode == "M_UNSUPPORTED_ROOM_VERSION"
479 || errCode == "M_INCOMPATIBLE_ROOM_VERSION")
480 return { UnsupportedRoomVersionError,
481 errorJson.contains("room_version"_ls)
482 ? tr("Requested room version: %1")
483 .arg(errorJson.value("room_version"_ls).toString())
484 : errorJson.value("error"_ls).toString() };
485 if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM")
486 return { CannotLeaveRoom,
487 tr("It's not allowed to leave a server notices room") };
488 if (errCode == "M_USER_DEACTIVATED")
489 return { UserDeactivated };
490
491 // Not localisable on the client side
492 if (errorJson.contains("error"_ls))
493 d->status.message = errorJson.value("error"_ls).toString();
494
495 return d->status;
496 }
497
stop()498 void BaseJob::stop()
499 {
500 // This method is (also) used to semi-finalise the job before retrying; so
501 // stop the timeout timer but keep the retry timer running.
502 d->timer.stop();
503 if (d->reply) {
504 d->reply->disconnect(this); // Ignore whatever comes from the reply
505 if (d->reply->isRunning()) {
506 qCWarning(d->logCat)
507 << this << "stopped without ready network reply";
508 d->reply->abort(); // Keep the reply object in case clients need it
509 }
510 } else
511 qCWarning(d->logCat) << this << "stopped with empty network reply";
512 }
513
finishJob()514 void BaseJob::finishJob()
515 {
516 stop();
517 if (error() == TooManyRequests) {
518 emit rateLimited();
519 d->connection->submit(this);
520 return;
521 }
522 if (error() == Unauthorised && !d->needsToken
523 && !d->connection->accessToken().isEmpty()) {
524 // Rerun with access token (extension of the spec while
525 // https://github.com/matrix-org/matrix-doc/issues/701 is pending)
526 d->connection->setNeedsToken(objectName());
527 qCWarning(d->logCat) << this << "re-running with authentication";
528 emit retryScheduled(d->retriesTaken, 0);
529 d->connection->submit(this);
530 return;
531 }
532 if ((error() == NetworkError || error() == Timeout)
533 && d->retriesTaken < d->maxRetries) {
534 // TODO: The whole retrying thing should be put to Connection(Manager)
535 // otherwise independently retrying jobs make a bit of notification
536 // storm towards the UI.
537 const seconds retryIn = error() == Timeout ? 0s : getNextRetryInterval();
538 ++d->retriesTaken;
539 qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken
540 << " in " << retryIn.count() << " s";
541 d->retryTimer.start(retryIn);
542 emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count());
543 return;
544 }
545
546 Q_ASSERT(status().code != Pending);
547
548 // Notify those interested in any completion of the job including abandon()
549 emit finished(this);
550
551 emit result(this); // abandon() doesn't emit this
552 if (error())
553 emit failure(this);
554 else
555 emit success(this);
556
557 deleteLater();
558 }
559
getCurrentTimeout() const560 seconds BaseJob::getCurrentTimeout() const
561 {
562 return d->getCurrentTimeoutConfig().jobTimeout;
563 }
564
getCurrentTimeoutMs() const565 BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const
566 {
567 return milliseconds(getCurrentTimeout()).count();
568 }
569
getNextRetryInterval() const570 seconds BaseJob::getNextRetryInterval() const
571 {
572 return d->getCurrentTimeoutConfig().nextRetryInterval;
573 }
574
getNextRetryMs() const575 BaseJob::duration_ms_t BaseJob::getNextRetryMs() const
576 {
577 return milliseconds(getNextRetryInterval()).count();
578 }
579
timeToRetry() const580 milliseconds BaseJob::timeToRetry() const
581 {
582 return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration()
583 : 0s;
584 }
585
millisToRetry() const586 BaseJob::duration_ms_t BaseJob::millisToRetry() const
587 {
588 return timeToRetry().count();
589 }
590
maxRetries() const591 int BaseJob::maxRetries() const { return d->maxRetries; }
592
setMaxRetries(int newMaxRetries)593 void BaseJob::setMaxRetries(int newMaxRetries)
594 {
595 d->maxRetries = newMaxRetries;
596 }
597
status() const598 BaseJob::Status BaseJob::status() const { return d->status; }
599
rawData(int bytesAtMost) const600 QByteArray BaseJob::rawData(int bytesAtMost) const
601 {
602 return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost
603 ? d->rawResponse.left(bytesAtMost)
604 : d->rawResponse;
605 }
606
rawDataSample(int bytesAtMost) const607 QString BaseJob::rawDataSample(int bytesAtMost) const
608 {
609 auto data = rawData(bytesAtMost);
610 Q_ASSERT(data.size() <= d->rawResponse.size());
611 return data.size() == d->rawResponse.size()
612 ? data
613 : data
614 + tr("...(truncated, %Ln bytes in total)",
615 "Comes after trimmed raw network response",
616 d->rawResponse.size());
617 }
618
statusCaption() const619 QString BaseJob::statusCaption() const
620 {
621 switch (d->status.code) {
622 case Success:
623 return tr("Success");
624 case Pending:
625 return tr("Request still pending response");
626 case UnexpectedResponseTypeWarning:
627 return tr("Warning: Unexpected response type");
628 case Abandoned:
629 return tr("Request was abandoned");
630 case NetworkError:
631 return tr("Network problems");
632 case TimeoutError:
633 return tr("Request timed out");
634 case Unauthorised:
635 return tr("Unauthorised request");
636 case ContentAccessError:
637 return tr("Access error");
638 case NotFoundError:
639 return tr("Not found");
640 case IncorrectRequestError:
641 return tr("Invalid request");
642 case IncorrectResponseError:
643 return tr("Response could not be parsed");
644 case TooManyRequestsError:
645 return tr("Too many requests");
646 case RequestNotImplementedError:
647 return tr("Function not implemented by the server");
648 case NetworkAuthRequiredError:
649 return tr("Network authentication required");
650 case UserConsentRequiredError:
651 return tr("User consent required");
652 case UnsupportedRoomVersionError:
653 return tr("The server does not support the needed room version");
654 default:
655 return tr("Request failed");
656 }
657 }
658
error() const659 int BaseJob::error() const { return d->status.code; }
660
errorString() const661 QString BaseJob::errorString() const { return d->status.message; }
662
errorUrl() const663 QUrl BaseJob::errorUrl() const { return d->errorUrl; }
664
setStatus(Status s)665 void BaseJob::setStatus(Status s)
666 {
667 // The crash that led to this code has been reported in
668 // https://github.com/quotient-im/Quaternion/issues/566 - basically,
669 // when cleaning up children of a deleted Connection, there's a chance
670 // of pending jobs being abandoned, calling setStatus(Abandoned).
671 // There's nothing wrong with this; however, the safety check for
672 // cleartext access tokens below uses d->connection - which is a dangling
673 // pointer.
674 // To alleviate that, a stricter condition is applied, that for Abandoned
675 // and to-be-Abandoned jobs the status message will be disregarded entirely.
676 // We could rectify the situation by making d->connection a QPointer<>
677 // (and deriving ConnectionData from QObject, respectively) but it's
678 // a too edge case for the hassle.
679 if (d->status == s)
680 return;
681
682 if (d->status.code == Abandoned || s.code == Abandoned)
683 s.message.clear();
684
685 if (!s.message.isEmpty() && d->connection
686 && !d->connection->accessToken().isEmpty())
687 s.message.replace(d->connection->accessToken(), "(REDACTED)");
688 if (!s.good())
689 qCWarning(d->logCat) << this << "status" << s;
690 d->status = std::move(s);
691 emit statusChanged(d->status);
692 }
693
setStatus(int code,QString message)694 void BaseJob::setStatus(int code, QString message)
695 {
696 setStatus({ code, std::move(message) });
697 }
698
abandon()699 void BaseJob::abandon()
700 {
701 beforeAbandon(d->reply ? d->reply.data() : nullptr);
702 d->timer.stop();
703 d->retryTimer.stop(); // In case abandon() was called between retries
704 setStatus(Abandoned);
705 if (d->reply)
706 d->reply->disconnect(this);
707 emit finished(this);
708
709 deleteLater();
710 }
711
timeout()712 void BaseJob::timeout()
713 {
714 setStatus(TimeoutError, "The job has timed out");
715 finishJob();
716 }
717
setLoggingCategory(LoggingCategory lcf)718 void BaseJob::setLoggingCategory(LoggingCategory lcf) { d->logCat = lcf; }
719