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 | |
16 | class QNetworkRequest; |
17 | class QNetworkReply; |
18 | class QSslError; |
19 | |
20 | namespace Quotient { |
21 | class ConnectionData; |
22 | |
23 | enum class HttpVerb { Get, Put, Post, Delete }; |
24 | |
25 | class 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 | |
38 | public: |
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 | |
141 | public: |
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 | |
252 | public 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 | |
262 | Q_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 | |
349 | protected: |
350 | using = QHash<QByteArray, QByteArray>; |
351 | |
352 | const headers_t& () const; |
353 | void (const headers_t::key_type& , |
354 | const headers_t::mapped_type& ); |
355 | void (const headers_t& ); |
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 | |
445 | protected 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 | |
468 | private Q_SLOTS: |
469 | void sendRequest(); |
470 | void gotReply(); |
471 | |
472 | friend class ConnectionData; // to provide access to sendRequest() |
473 | |
474 | private: |
475 | void stop(); |
476 | void finishJob(); |
477 | |
478 | class Private; |
479 | ImplPtr<Private> d; |
480 | }; |
481 | |
482 | inline bool QUOTIENT_API isJobPending(BaseJob* job) |
483 | { |
484 | return job && job->error() == BaseJob::Pending; |
485 | } |
486 | } // namespace Quotient |
487 | |