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