1// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de>
2// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
3// SPDX-License-Identifier: LGPL-2.1-or-later
4
5#pragma once
6
7#include "requestdata.h"
8
9#include <QtCore/QObject>
10#include <QtCore/QStringBuilder>
11#include <QtCore/QLoggingCategory>
12
13#include <Quotient/converters.h> // Common for csapi/ headers even though not used here
14#include <Quotient/quotient_common.h> // For DECL_DEPRECATED_ENUMERATOR
15
16class QNetworkRequest;
17class QNetworkReply;
18class QSslError;
19
20namespace Quotient {
21class ConnectionData;
22
23enum class HttpVerb { Get, Put, Post, Delete };
24
25class QUOTIENT_API BaseJob : public QObject {
26 Q_OBJECT
27 Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
28 Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
29 Q_PROPERTY(int statusCode READ error NOTIFY statusChanged)
30
31 static QByteArray encodeIfParam(const QString& paramPart);
32 template <int N>
33 static auto encodeIfParam(const char (&constPart)[N])
34 {
35 return constPart;
36 }
37
38public:
39#define WITH_DEPRECATED_ERROR_VERSION(Recommended) \
40 Recommended, DECL_DEPRECATED_ENUMERATOR(Recommended##Error, Recommended)
41
42 /*! The status code of a job
43 *
44 * Every job is created in Unprepared status; upon calling prepare()
45 * from Connection (if things are fine) it go to Pending status. After
46 * that, the next transition comes after the reply arrives and its contents
47 * are analysed. At any point in time the job can be abandon()ed, causing
48 * it to switch to status Abandoned for a brief period before deletion.
49 */
50 enum StatusCode {
51 Success = 0,
52 NoError = Success,
53 Pending = 1,
54 WarningLevel = 20, //!< Warnings have codes starting from this
55 UnexpectedResponseType = 21,
56 UnexpectedResponseTypeWarning = UnexpectedResponseType,
57 Unprepared = 25, //!< Initial job state is incomplete, hence warning level
58 Abandoned = 50, //!< A tiny period between abandoning and object deletion
59 ErrorLevel = 100, //!< Errors have codes starting from this
60 NetworkError = 101,
61 WITH_DEPRECATED_ERROR_VERSION(Timeout),
62 Unauthorised,
63 ContentAccessError,
64 WITH_DEPRECATED_ERROR_VERSION(NotFound),
65 WITH_DEPRECATED_ERROR_VERSION(IncorrectRequest),
66 WITH_DEPRECATED_ERROR_VERSION(IncorrectResponse),
67 WITH_DEPRECATED_ERROR_VERSION(TooManyRequests),
68 RateLimited = TooManyRequests,
69 WITH_DEPRECATED_ERROR_VERSION(RequestNotImplemented),
70 WITH_DEPRECATED_ERROR_VERSION(UnsupportedRoomVersion),
71 WITH_DEPRECATED_ERROR_VERSION(NetworkAuthRequired),
72 WITH_DEPRECATED_ERROR_VERSION(UserConsentRequired),
73 CannotLeaveRoom,
74 UserDeactivated,
75 FileError,
76 UserDefinedError = 256
77 };
78 Q_ENUM(StatusCode)
79
80#undef WITH_DEPRECATED_ERROR_VERSION
81
82 template <typename... StrTs>
83 static QByteArray makePath(StrTs&&... parts)
84 {
85 return (QByteArray() % ... % encodeIfParam(parts));
86 }
87
88 using Data
89#ifndef Q_CC_MSVC
90 Q_DECL_DEPRECATED_X("Use Quotient::RequestData instead")
91#endif
92 = RequestData;
93
94 /*!
95 * This structure stores the status of a server call job. The status
96 * consists of a code, that is described (but not delimited) by the
97 * respective enum, and a freeform message.
98 *
99 * To extend the list of error codes, define an (anonymous) enum
100 * along the lines of StatusCode, with additional values
101 * starting at UserDefinedError
102 */
103 struct Status {
104 Status(StatusCode c) : code(c) {}
105 Status(int c, QString m) : code(c), message(std::move(m)) {}
106
107 static StatusCode fromHttpCode(int httpCode);
108 static Status fromHttpCode(int httpCode, QString msg)
109 {
110 return { fromHttpCode(httpCode), std::move(msg) };
111 }
112
113 bool good() const { return code < ErrorLevel; }
114 QDebug dumpToLog(QDebug dbg) const;
115 friend QDebug operator<<(const QDebug& dbg, const Status& s)
116 {
117 return s.dumpToLog(dbg);
118 }
119
120 bool operator==(const Status& other) const
121 {
122 return code == other.code && message == other.message;
123 }
124 bool operator!=(const Status& other) const
125 {
126 return !operator==(other);
127 }
128 bool operator==(int otherCode) const
129 {
130 return code == otherCode;
131 }
132 bool operator!=(int otherCode) const
133 {
134 return !operator==(otherCode);
135 }
136
137 int code;
138 QString message;
139 };
140
141public:
142 BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
143 bool needsToken = true);
144 BaseJob(HttpVerb verb, const QString& name, QByteArray endpoint,
145 const QUrlQuery& query, RequestData&& data = {},
146 bool needsToken = true);
147
148 QUrl requestUrl() const;
149 bool isBackground() const;
150
151 /** Current status of the job */
152 Status status() const;
153
154 /** Short human-friendly message on the job status */
155 QString statusCaption() const;
156
157 /*! Get first bytes of the raw response body as received from the server
158 *
159 * \param bytesAtMost the number of leftmost bytes to return
160 *
161 * \sa rawDataSample
162 */
163 QByteArray rawData(int bytesAtMost) const;
164
165 /*! Access the whole response body as received from the server */
166 const QByteArray& rawData() const;
167
168 /** Get UI-friendly sample of raw data
169 *
170 * This is almost the same as rawData but appends the "truncated"
171 * suffix if not all data fit in bytesAtMost. This call is
172 * recommended to present a sample of raw data as "details" next to
173 * error messages. Note that the default \p bytesAtMost value is
174 * also tailored to UI cases.
175 *
176 * \sa rawData
177 */
178 QString rawDataSample(int bytesAtMost = 65535) const;
179
180 /** Get the response body as a JSON object
181 *
182 * If the job's returned content type is not `application/json`
183 * or if the top-level JSON entity is not an object, an empty object
184 * is returned.
185 */
186 QJsonObject jsonData() const;
187
188 /** Get the response body as a JSON array
189 *
190 * If the job's returned content type is not `application/json`
191 * or if the top-level JSON entity is not an array, an empty array
192 * is returned.
193 */
194 QJsonArray jsonItems() const;
195
196 /** Load the property from the JSON response assuming a given C++ type
197 *
198 * If there's no top-level JSON object in the response or if there's
199 * no node with the key \p keyName, \p defaultValue is returned.
200 */
201 template <typename T, typename StrT>
202 T loadFromJson(const StrT& keyName, T&& defaultValue = {}) const
203 {
204 const auto& jv = jsonData().value(keyName);
205 return jv.isUndefined() ? std::forward<T>(defaultValue)
206 : fromJson<T>(jv);
207 }
208
209 /** Load the property from the JSON response and delete it from JSON
210 *
211 * If there's no top-level JSON object in the response or if there's
212 * no node with the key \p keyName, \p defaultValue is returned.
213 */
214 template <typename T>
215 T takeFromJson(const QString& key, T&& defaultValue = {})
216 {
217 if (const auto& jv = takeValueFromJson(key); !jv.isUndefined())
218 return fromJson<T>(jv);
219
220 return std::forward<T>(defaultValue);
221 }
222
223 /** Error (more generally, status) code
224 * Equivalent to status().code
225 * \sa status
226 */
227 int error() const;
228
229 /** Error-specific message, as returned by the server */
230 virtual QString errorString() const;
231
232 /** A URL to help/clarify the error, if provided by the server */
233 QUrl errorUrl() const;
234
235 int maxRetries() const;
236 void setMaxRetries(int newMaxRetries);
237
238 using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t
239
240 std::chrono::seconds getCurrentTimeout() const;
241 Q_INVOKABLE Quotient::BaseJob::duration_ms_t getCurrentTimeoutMs() const;
242 std::chrono::seconds getNextRetryInterval() const;
243 Q_INVOKABLE Quotient::BaseJob::duration_ms_t getNextRetryMs() const;
244 std::chrono::milliseconds timeToRetry() const;
245 Q_INVOKABLE Quotient::BaseJob::duration_ms_t millisToRetry() const;
246
247 friend QDebug operator<<(QDebug dbg, const BaseJob* j)
248 {
249 return dbg << j->objectName();
250 }
251
252public Q_SLOTS:
253 void initiate(Quotient::ConnectionData* connData, bool inBackground);
254
255 //! \brief Abandon the result of this job, arrived or unarrived.
256 //!
257 //! This aborts waiting for a reply from the server (if there was
258 //! any pending) and deletes the job object. No result signals
259 //! (result, success, failure) are emitted, only finished() is.
260 void abandon();
261
262Q_SIGNALS:
263 //! \brief The job is about to send a network request
264 //!
265 //! This signal is emitted every time a network request is made (which can
266 //! occur several times due to job retries). You can use it to change
267 //! the request parameters (such as redirect policy) if necessary. If you
268 //! need to set additional request headers or query items, do that using
269 //! setRequestHeaders() and setRequestQuery() instead.
270 //! \note \p req is not guaranteed to exist (i.e. it may point to garbage)
271 //! unless this signal is handled via a DirectConnection (or
272 //! BlockingQueuedConnection if in another thread), i.e.,
273 //! synchronously.
274 //! \sa setRequestHeaders, setRequestQuery
275 void aboutToSendRequest(QNetworkRequest* req);
276
277 /** The job has sent a network request */
278 void sentRequest();
279
280 /** The job has changed its status */
281 void statusChanged(Quotient::BaseJob::Status newStatus);
282
283 /**
284 * The previous network request has failed; the next attempt will
285 * be done in the specified time
286 * @param nextAttempt the 1-based number of attempt (will always be more
287 * than 1)
288 * @param inMilliseconds the interval after which the next attempt will be
289 * taken
290 */
291 void retryScheduled(int nextAttempt,
292 Quotient::BaseJob::duration_ms_t inMilliseconds);
293
294 /**
295 * The previous network request has been rate-limited; the next attempt
296 * will be queued and run sometime later. Since other jobs may already
297 * wait in the queue, it's not possible to predict the wait time.
298 */
299 void rateLimited();
300
301 /**
302 * Emitted when the job is finished, in any case. It is used to notify
303 * observers that the job is terminated and that progress can be hidden.
304 *
305 * This should not be emitted directly by subclasses;
306 * use finishJob() instead.
307 *
308 * In general, to be notified of a job's completion, client code
309 * should connect to result(), success(), or failure()
310 * rather than finished(). However if you need to track the job's
311 * lifecycle you should connect to this instead of result();
312 * in particular, only this signal will be emitted on abandoning.
313 *
314 * @param job the job that emitted this signal
315 *
316 * @see result, success, failure
317 */
318 void finished(Quotient::BaseJob* job);
319
320 /**
321 * Emitted when the job is finished (except when abandoned).
322 *
323 * Use error() to know if the job was finished with error.
324 *
325 * @param job the job that emitted this signal
326 *
327 * @see success, failure
328 */
329 void result(Quotient::BaseJob* job);
330
331 /**
332 * Emitted together with result() in case there's no error.
333 *
334 * @see result, failure
335 */
336 void success(Quotient::BaseJob*);
337
338 /**
339 * Emitted together with result() if there's an error.
340 * Similar to result(), this won't be emitted in case of abandon().
341 *
342 * @see result, success
343 */
344 void failure(Quotient::BaseJob*);
345
346 void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
347 void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
348
349protected:
350 using headers_t = QHash<QByteArray, QByteArray>;
351
352 const headers_t& requestHeaders() const;
353 void setRequestHeader(const headers_t::key_type& headerName,
354 const headers_t::mapped_type& headerValue);
355 void setRequestHeaders(const headers_t& headers);
356 QUrlQuery query() const;
357 void setRequestQuery(const QUrlQuery& query);
358 const RequestData& requestData() const;
359 void setRequestData(RequestData&& data);
360 const QByteArrayList& expectedContentTypes() const;
361 void addExpectedContentType(const QByteArray& contentType);
362 void setExpectedContentTypes(const QByteArrayList& contentTypes);
363 QByteArrayList expectedKeys() const;
364 void addExpectedKey(const QByteArray &key);
365 void setExpectedKeys(const QByteArrayList &keys);
366
367 const QNetworkReply* reply() const;
368 QNetworkReply* reply();
369
370 /** Construct a URL out of baseUrl, path and query
371 *
372 * The function ensures exactly one '/' between the path component of
373 * \p baseUrl and \p path. The query component of \p baseUrl is ignored.
374 */
375 static QUrl makeRequestUrl(QUrl baseUrl, const QByteArray &encodedPath,
376 const QUrlQuery& query = {});
377
378 /*! Prepares the job for execution
379 *
380 * This method is called no more than once per job lifecycle,
381 * when it's first scheduled for execution; in particular, it is not called
382 * on retries.
383 */
384 virtual void doPrepare();
385
386 /*! Postprocessing after the network request has been sent
387 *
388 * This method is called every time the job receives a running
389 * QNetworkReply object from NetworkAccessManager - basically, after
390 * successfully sending a network request (including retries).
391 */
392 virtual void onSentRequest(QNetworkReply*);
393 virtual void beforeAbandon();
394
395 /*! \brief An extension point for additional reply processing.
396 *
397 * The base implementation does nothing and returns Success.
398 *
399 * \sa gotReply
400 */
401 virtual Status prepareResult();
402
403 /*! \brief Process details of the error
404 *
405 * The function processes the reply in case when status from checkReply()
406 * was not good (usually because of an unsuccessful HTTP code).
407 * The base implementation assumes Matrix JSON error object in the body;
408 * overrides are strongly recommended to call it for all stock Matrix
409 * responses as early as possible and only then process custom errors,
410 * with JSON or non-JSON payload.
411 *
412 * \return updated (if necessary) job status
413 */
414 virtual Status prepareError(Status currentStatus);
415
416 /*! \brief Get direct access to the JSON response object in the job
417 *
418 * This allows to implement deserialisation with "move" semantics for parts
419 * of the response. Assuming that the response body is a valid JSON object,
420 * the function calls QJsonObject::take(key) on it and returns the result.
421 *
422 * \return QJsonValue::Null, if the response content type is not
423 * advertised as `application/json`;
424 * QJsonValue::Undefined, if the response is a JSON object but
425 * doesn't have \p key;
426 * the value for \p key otherwise.
427 *
428 * \sa takeFromJson
429 */
430 QJsonValue takeValueFromJson(const QString& key);
431
432 void setStatus(Status s);
433 void setStatus(int code, QString message);
434
435 //! \brief Set the logging category for the given job instance
436 //!
437 //! \param lcf The logging category function to provide the category -
438 //! the one you define with Q_LOGGING_CATEGORY (without
439 //! parentheses, BaseJob will call it for you)
440 void setLoggingCategory(QMessageLogger::CategoryFunction lcf);
441
442 // Job objects should only be deleted via QObject::deleteLater
443 ~BaseJob() override;
444
445protected Q_SLOTS:
446 void timeout();
447
448 /*! \brief Check the pending or received reply for upfront issues
449 *
450 * This is invoked when headers are first received and also once
451 * the complete reply is obtained; the base implementation checks the HTTP
452 * headers to detect general issues such as network errors or access denial
453 * and it's strongly recommended to call it from overrides,
454 * as early as possible.
455 * This slot is const and cannot read the response body. If you need to read
456 * the body on the fly, override onSentRequest() and connect in it
457 * to reply->readyRead(); and if you only need to validate the body after
458 * it fully arrived, use prepareResult() for that). Returning anything
459 * except NoError/Success switches further processing from prepareResult()
460 * to prepareError().
461 *
462 * @return the result of checking the reply
463 *
464 * @see gotReply
465 */
466 virtual Status checkReply(const QNetworkReply *reply) const;
467
468private Q_SLOTS:
469 void sendRequest();
470 void gotReply();
471
472 friend class ConnectionData; // to provide access to sendRequest()
473
474private:
475 void stop();
476 void finishJob();
477
478 class Private;
479 ImplPtr<Private> d;
480};
481
482inline bool QUOTIENT_API isJobPending(BaseJob* job)
483{
484 return job && job->error() == BaseJob::Pending;
485}
486} // namespace Quotient
487