cutelyst  4.4.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
session.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2013-2022 Daniel Nicoletti <dantti12@gmail.com>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 #include "session_p.h"
6 #include "sessionstorefile.h"
7 #include "utils.h"
8 
9 #include <Cutelyst/Application>
10 #include <Cutelyst/Context>
11 #include <Cutelyst/Engine>
12 #include <Cutelyst/Response>
13 
14 #include <QCoreApplication>
15 #include <QHostAddress>
16 #include <QLoggingCategory>
17 #include <QUuid>
18 
19 using namespace Cutelyst;
20 
21 Q_LOGGING_CATEGORY(C_SESSION, "cutelyst.plugin.session", QtWarningMsg)
22 
23 #define SESSION_VALUES QStringLiteral("_c_session_values")
24 #define SESSION_EXPIRES QStringLiteral("_c_session_expires")
25 #define SESSION_TRIED_LOADING_EXPIRES QStringLiteral("_c_session_tried_loading_expires")
26 #define SESSION_EXTENDED_EXPIRES QStringLiteral("_c_session_extended_expires")
27 #define SESSION_UPDATED QStringLiteral("_c_session_updated")
28 #define SESSION_ID QStringLiteral("_c_session_id")
29 #define SESSION_TRIED_LOADING_ID QStringLiteral("_c_session_tried_loading_id")
30 #define SESSION_DELETED_ID QStringLiteral("_c_session_deleted_id")
31 #define SESSION_DELETE_REASON QStringLiteral("_c_session_delete_reason")
32 
33 static thread_local Session *m_instance = nullptr;
34 
36  : Plugin(parent)
37  , d_ptr(new SessionPrivate(this))
38 {
39 }
40 
41 Session::Session(Cutelyst::Application *parent, const QVariantMap &defaultConfig)
42  : Plugin(parent)
43  , d_ptr(new SessionPrivate(this))
44 {
45  d_ptr->defaultConfig = defaultConfig;
46 }
47 
49 {
50  delete d_ptr;
51 }
52 
54 {
55  Q_D(Session);
56  d->sessionName = QCoreApplication::applicationName().toLatin1() + "_session";
57 
58  d->loadedConfig = app->engine()->config(u"Cutelyst_Session_Plugin"_qs);
59  d->sessionExpires = std::chrono::duration_cast<std::chrono::seconds>(
60  Utils::durationFromString(d->config(u"expires"_qs, 7200).toString()))
61  .count();
62  d->expiryThreshold = d->config(u"expiry_threshold"_qs, 0).toLongLong();
63  d->verifyAddress = d->config(u"verify_address"_qs, false).toBool();
64  d->verifyUserAgent = d->config(u"verify_user_agent"_qs, false).toBool();
65  d->cookieHttpOnly = d->config(u"cookie_http_only"_qs, true).toBool();
66  d->cookieSecure = d->config(u"cookie_secure"_qs, false).toBool();
67 
68  const QString _sameSite = d->config(u"cookie_same_site"_qs, u"strict"_qs).toString();
69  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
70  d->cookieSameSite = QNetworkCookie::SameSite::Default;
71  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
72  d->cookieSameSite = QNetworkCookie::SameSite::None;
73  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
74  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
75  } else {
76  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
77  }
78 
79  connect(app, &Application::afterDispatch, this, &SessionPrivate::_q_saveSession);
80  connect(app, &Application::postForked, this, [this] { m_instance = this; });
81 
82  if (!d->store) {
83  d->store = std::make_unique<SessionStoreFile>(this);
84  }
85 
86  return true;
87 }
88 
89 void Session::setStorage(std::unique_ptr<Cutelyst::SessionStore> store)
90 {
91  Q_D(Session);
92  Q_ASSERT_X(d->store, "Cutelyst::Session::setStorage", "Session Storage is alread defined");
93  store->setParent(this);
94  d->store = std::move(store);
95 }
96 
98 {
99  Q_D(const Session);
100  return d->store.get();
101 }
102 
104 {
105  QByteArray ret;
106  const QVariant sid = c->stash(SESSION_ID);
107  if (sid.isNull()) {
108  if (Q_UNLIKELY(!m_instance)) {
109  qCCritical(C_SESSION) << "Session plugin not registered";
110  return ret;
111  }
112 
113  ret = SessionPrivate::loadSessionId(c, m_instance->d_ptr->sessionName);
114  } else {
115  ret = sid.toByteArray();
116  }
117 
118  return ret;
119 }
120 
122 {
123  QVariant expires = c->stash(SESSION_EXTENDED_EXPIRES);
124  if (!expires.isNull()) {
125  return expires.toULongLong();
126  }
127 
128  if (Q_UNLIKELY(!m_instance)) {
129  qCCritical(C_SESSION) << "Session plugin not registered";
130  return 0;
131  }
132 
133  expires = SessionPrivate::loadSessionExpires(m_instance, c, id(c));
134  if (!expires.isNull()) {
135  return quint64(SessionPrivate::extendSessionExpires(m_instance, c, expires.toLongLong()));
136  }
137 
138  return 0;
139 }
140 
141 void Session::changeExpires(Context *c, quint64 expires)
142 {
143  const QByteArray sid = Session::id(c);
144  const qint64 timeExp = QDateTime::currentSecsSinceEpoch() + qint64(expires);
145 
146  if (Q_UNLIKELY(!m_instance)) {
147  qCCritical(C_SESSION) << "Session plugin not registered";
148  return;
149  }
150 
151  m_instance->d_ptr->store->storeSessionData(c, sid, u"expires"_qs, timeExp);
152 }
153 
154 void Session::deleteSession(Context *c, const QString &reason)
155 {
156  if (Q_UNLIKELY(!m_instance)) {
157  qCCritical(C_SESSION) << "Session plugin not registered";
158  return;
159  }
160  SessionPrivate::deleteSession(m_instance, c, reason);
161 }
162 
164 {
165  return c->stash(SESSION_DELETE_REASON).toString();
166 }
167 
168 QVariant Session::value(Cutelyst::Context *c, const QString &key, const QVariant &defaultValue)
169 {
170  QVariant ret = defaultValue;
171  QVariant session = c->stash(SESSION_VALUES);
172  if (session.isNull()) {
173  session = SessionPrivate::loadSession(c);
174  }
175 
176  if (!session.isNull()) {
177  ret = session.toHash().value(key, defaultValue);
178  }
179 
180  return ret;
181 }
182 
183 void Session::setValue(Cutelyst::Context *c, const QString &key, const QVariant &value)
184 {
185  QVariant session = c->stash(SESSION_VALUES);
186  if (session.isNull()) {
187  session = SessionPrivate::loadSession(c);
188  if (session.isNull()) {
189  if (Q_UNLIKELY(!m_instance)) {
190  qCCritical(C_SESSION) << "Session plugin not registered";
191  return;
192  }
193 
194  SessionPrivate::createSessionIdIfNeeded(
195  m_instance, c, m_instance->d_ptr->sessionExpires);
196  session = SessionPrivate::initializeSessionData(m_instance, c);
197  }
198  }
199 
200  QVariantHash data = session.toHash();
201  data.insert(key, value);
202 
203  c->setStash(SESSION_VALUES, data);
204  c->setStash(SESSION_UPDATED, true);
205 }
206 
208 {
209  QVariant session = c->stash(SESSION_VALUES);
210  if (session.isNull()) {
211  session = SessionPrivate::loadSession(c);
212  if (session.isNull()) {
213  if (Q_UNLIKELY(!m_instance)) {
214  qCCritical(C_SESSION) << "Session plugin not registered";
215  return;
216  }
217 
218  SessionPrivate::createSessionIdIfNeeded(
219  m_instance, c, m_instance->d_ptr->sessionExpires);
220  session = SessionPrivate::initializeSessionData(m_instance, c);
221  }
222  }
223 
224  QVariantHash data = session.toHash();
225  data.remove(key);
226 
227  c->setStash(SESSION_VALUES, data);
228  c->setStash(SESSION_UPDATED, true);
229 }
230 
232 {
233  QVariant session = c->stash(SESSION_VALUES);
234  if (session.isNull()) {
235  session = SessionPrivate::loadSession(c);
236  if (session.isNull()) {
237  if (Q_UNLIKELY(!m_instance)) {
238  qCCritical(C_SESSION) << "Session plugin not registered";
239  return;
240  }
241 
242  SessionPrivate::createSessionIdIfNeeded(
243  m_instance, c, m_instance->d_ptr->sessionExpires);
244  session = SessionPrivate::initializeSessionData(m_instance, c);
245  }
246  }
247 
248  QVariantHash data = session.toHash();
249  for (const QString &key : keys) {
250  data.remove(key);
251  }
252 
253  c->setStash(SESSION_VALUES, data);
254  c->setStash(SESSION_UPDATED, true);
255 }
256 
258 {
259  return !SessionPrivate::loadSession(c).isNull();
260 }
261 
262 QByteArray SessionPrivate::generateSessionId()
263 {
264  return QUuid::createUuid().toRfc4122().toHex();
265 }
266 
267 QByteArray SessionPrivate::loadSessionId(Context *c, const QByteArray &sessionName)
268 {
269  QByteArray ret;
270  if (!c->stash(SESSION_TRIED_LOADING_ID).isNull()) {
271  return ret;
272  }
273  c->setStash(SESSION_TRIED_LOADING_ID, true);
274 
275  const QByteArray sid = getSessionId(c, sessionName);
276  if (!sid.isEmpty()) {
277  if (!validateSessionId(sid)) {
278  qCCritical(C_SESSION) << "Tried to set invalid session ID" << sid;
279  return ret;
280  }
281  ret = sid;
282  c->setStash(SESSION_ID, sid);
283  }
284 
285  return ret;
286 }
287 
288 QByteArray SessionPrivate::getSessionId(Context *c, const QByteArray &sessionName)
289 {
290  QByteArray ret;
291  bool deleted = !c->stash(SESSION_DELETED_ID).isNull();
292 
293  if (!deleted) {
294  const QVariant property = c->stash(SESSION_ID);
295  if (!property.isNull()) {
296  ret = property.toByteArray();
297  return ret;
298  }
299 
300  const QByteArray cookie = c->request()->cookie(sessionName);
301  if (!cookie.isEmpty()) {
302  qCDebug(C_SESSION) << "Found sessionid" << cookie << "in cookie";
303  ret = cookie;
304  }
305  }
306 
307  return ret;
308 }
309 
310 QByteArray SessionPrivate::createSessionIdIfNeeded(Session *session, Context *c, qint64 expires)
311 {
312  QByteArray ret;
313  const QVariant sid = c->stash(SESSION_ID);
314  if (!sid.isNull()) {
315  ret = sid.toByteArray();
316  } else {
317  ret = createSessionId(session, c, expires);
318  }
319  return ret;
320 }
321 
322 QByteArray SessionPrivate::createSessionId(Session *session, Context *c, qint64 expires)
323 {
324  Q_UNUSED(expires)
325  const auto sid = generateSessionId();
326 
327  qCDebug(C_SESSION) << "Created session" << sid;
328 
329  c->setStash(SESSION_ID, sid);
330  resetSessionExpires(session, c, sid);
331  setSessionId(session, c, sid);
332 
333  return sid;
334 }
335 
336 void SessionPrivate::_q_saveSession(Context *c)
337 {
338  // fix cookie before we send headers
339  saveSessionExpires(c);
340 
341  // Force extension of session_expires before finalizing headers, so a pos
342  // up to date. First call to session_expires will extend the expiry, methods
343  // just return the previously extended value.
344  Session::expires(c);
345 
346  // Persist data
347  if (Q_UNLIKELY(!m_instance)) {
348  qCCritical(C_SESSION) << "Session plugin not registered";
349  return;
350  }
351  saveSessionExpires(c);
352 
353  if (!c->stash(SESSION_UPDATED).toBool()) {
354  return;
355  }
356  QVariantHash sessionData = c->stash(SESSION_VALUES).toHash();
357  sessionData.insert(QStringLiteral("__updated"), QDateTime::currentSecsSinceEpoch());
358 
359  const auto sid = c->stash(SESSION_ID).toByteArray();
360  m_instance->d_ptr->store->storeSessionData(c, sid, QStringLiteral("session"), sessionData);
361 }
362 
363 void SessionPrivate::deleteSession(Session *session, Context *c, const QString &reason)
364 {
365  qCDebug(C_SESSION) << "Deleting session" << reason;
366 
367  const QVariant sidVar = c->stash(SESSION_ID).toString();
368  if (!sidVar.isNull()) {
369  const auto sid = sidVar.toByteArray();
370  session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral("session"));
371  session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral("expires"));
372  session->d_ptr->store->deleteSessionData(c, sid, QStringLiteral("flash"));
373 
374  deleteSessionId(session, c, sid);
375  }
376 
377  // Reset the values in Context object
378  c->setStash(SESSION_VALUES, QVariant());
379  c->setStash(SESSION_ID, QVariant());
380  c->setStash(SESSION_EXPIRES, QVariant());
381 
382  c->setStash(SESSION_DELETE_REASON, reason);
383 }
384 
385 void SessionPrivate::deleteSessionId(Session *session, Context *c, const QByteArray &sid)
386 {
387  c->setStash(SESSION_DELETED_ID, true); // to prevent get_session_id from returning it
388 
389  updateSessionCookie(c, makeSessionCookie(session, c, sid, QDateTime::currentDateTimeUtc()));
390 }
391 
392 QVariant SessionPrivate::loadSession(Context *c)
393 {
394  QVariant ret;
395  const QVariant property = c->stash(SESSION_VALUES);
396  if (!property.isNull()) {
397  ret = property.toHash();
398  return ret;
399  }
400 
401  if (Q_UNLIKELY(!m_instance)) {
402  qCCritical(C_SESSION) << "Session plugin not registered";
403  return ret;
404  }
405 
406  const auto sid = Session::id(c);
407  if (!loadSessionExpires(m_instance, c, sid).isNull()) {
408  if (SessionPrivate::validateSessionId(sid)) {
409 
410  const QVariantHash sessionData =
411  m_instance->d_ptr->store->getSessionData(c, sid, QStringLiteral("session"))
412  .toHash();
413  c->setStash(SESSION_VALUES, sessionData);
414 
415  if (m_instance->d_ptr->verifyAddress) {
416  auto it = sessionData.constFind(u"__address"_qs);
417  if (it != sessionData.constEnd() &&
418  it->toString() != c->request()->address().toString()) {
419  qCWarning(C_SESSION)
420  << "Deleting session" << sid << "due to address mismatch:" << *it
421  << "!=" << c->request()->address().toString();
422  deleteSession(m_instance, c, QStringLiteral("address mismatch"));
423  return ret;
424  }
425  }
426 
427  if (m_instance->d_ptr->verifyUserAgent) {
428  auto it = sessionData.constFind(u"__user_agent"_qs);
429  if (it != sessionData.constEnd() &&
430  it->toByteArray() != c->request()->userAgent()) {
431  qCWarning(C_SESSION)
432  << "Deleting session" << sid << "due to user agent mismatch:" << *it
433  << "!=" << c->request()->userAgent();
434  deleteSession(m_instance, c, QStringLiteral("user agent mismatch"));
435  return ret;
436  }
437  }
438 
439  qCDebug(C_SESSION) << "Restored session" << sid << "keys" << sessionData.size();
440 
441  ret = sessionData;
442  }
443  }
444 
445  return ret;
446 }
447 
448 bool SessionPrivate::validateSessionId(QByteArrayView id)
449 {
450  auto it = id.begin();
451  auto end = id.end();
452  while (it != end) {
453  char c = *it;
454  if ((c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')) {
455  ++it;
456  continue;
457  }
458  return false;
459  }
460 
461  return id.size();
462 }
463 
464 qint64 SessionPrivate::extendSessionExpires(Session *session, Context *c, qint64 expires)
465 {
466  const qint64 threshold = qint64(session->d_ptr->expiryThreshold);
467 
468  const auto sid = Session::id(c);
469  if (!sid.isEmpty()) {
470  const qint64 current = getStoredSessionExpires(session, c, sid);
471  const qint64 cutoff = current - threshold;
472  const qint64 time = QDateTime::currentSecsSinceEpoch();
473 
474  if (!threshold || cutoff <= time || c->stash(SESSION_UPDATED).toBool()) {
475  qint64 updated = calculateInitialSessionExpires(session, c, sid);
476  c->setStash(SESSION_EXTENDED_EXPIRES, updated);
477  extendSessionId(session, c, sid, updated);
478 
479  return updated;
480  } else {
481  return current;
482  }
483  } else {
484  return expires;
485  }
486 }
487 
488 qint64 SessionPrivate::getStoredSessionExpires(Session *session,
489  Context *c,
490  const QByteArray &sessionid)
491 {
492  const QVariant expires =
493  session->d_ptr->store->getSessionData(c, sessionid, QStringLiteral("expires"), 0);
494  return expires.toLongLong();
495 }
496 
497 QVariant SessionPrivate::initializeSessionData(Session *session, Context *c)
498 {
499  QVariantHash ret;
500  const qint64 now = QDateTime::currentSecsSinceEpoch();
501  ret.insert(QStringLiteral("__created"), now);
502  ret.insert(QStringLiteral("__updated"), now);
503 
504  if (session->d_ptr->verifyAddress) {
505  ret.insert(QStringLiteral("__address"), c->request()->address().toString());
506  }
507 
508  if (session->d_ptr->verifyUserAgent) {
509  ret.insert(QStringLiteral("__user_agent"), c->request()->userAgent());
510  }
511 
512  return ret;
513 }
514 
515 void SessionPrivate::saveSessionExpires(Context *c)
516 {
517  const QVariant expires = c->stash(SESSION_EXPIRES);
518  if (!expires.isNull()) {
519  const auto sid = Session::id(c);
520  if (!sid.isEmpty()) {
521  if (Q_UNLIKELY(!m_instance)) {
522  qCCritical(C_SESSION) << "Session plugin not registered";
523  return;
524  }
525 
526  const qint64 current = getStoredSessionExpires(m_instance, c, sid);
527  const qint64 extended = qint64(Session::expires(c));
528  if (extended > current) {
529  m_instance->d_ptr->store->storeSessionData(
530  c, sid, QStringLiteral("expires"), extended);
531  }
532  }
533  }
534 }
535 
536 QVariant
537  SessionPrivate::loadSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
538 {
539  QVariant ret;
540  if (c->stash(SESSION_TRIED_LOADING_EXPIRES).toBool()) {
541  ret = c->stash(SESSION_EXPIRES);
542  return ret;
543  }
544  c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
545 
546  if (!sessionId.isEmpty()) {
547  const qint64 expires = getStoredSessionExpires(session, c, sessionId);
548 
549  if (expires >= QDateTime::currentSecsSinceEpoch()) {
550  c->setStash(SESSION_EXPIRES, expires);
551  ret = expires;
552  } else {
553  deleteSession(session, c, QStringLiteral("session expired"));
554  ret = 0;
555  }
556  }
557  return ret;
558 }
559 
560 qint64 SessionPrivate::initialSessionExpires(Session *session, Context *c)
561 {
562  Q_UNUSED(c)
563  const qint64 expires = qint64(session->d_ptr->sessionExpires);
564  return QDateTime::currentSecsSinceEpoch() + expires;
565 }
566 
567 qint64 SessionPrivate::calculateInitialSessionExpires(Session *session,
568  Context *c,
569  const QByteArray &sessionId)
570 {
571  const qint64 stored = getStoredSessionExpires(session, c, sessionId);
572  const qint64 initial = initialSessionExpires(session, c);
573  return qMax(initial, stored);
574 }
575 
576 qint64
577  SessionPrivate::resetSessionExpires(Session *session, Context *c, const QByteArray &sessionId)
578 {
579  const qint64 exp = calculateInitialSessionExpires(session, c, sessionId);
580 
581  c->setStash(SESSION_EXPIRES, exp);
582 
583  // since we're setting _session_expires directly, make loadSessionExpires
584  // actually use that value.
585  c->setStash(SESSION_TRIED_LOADING_EXPIRES, true);
586  c->setStash(SESSION_EXTENDED_EXPIRES, exp);
587 
588  return exp;
589 }
590 
591 void SessionPrivate::updateSessionCookie(Context *c, const QNetworkCookie &updated)
592 {
593  c->response()->setCookie(updated);
594 }
595 
596 QNetworkCookie SessionPrivate::makeSessionCookie(Session *session,
597  Context *c,
598  const QByteArray &sid,
599  const QDateTime &expires)
600 {
601  Q_UNUSED(c)
602  QNetworkCookie cookie(session->d_ptr->sessionName, sid);
603  cookie.setPath(u"/"_qs);
604  cookie.setExpirationDate(expires);
605  cookie.setHttpOnly(session->d_ptr->cookieHttpOnly);
606  cookie.setSecure(session->d_ptr->cookieSecure);
607  cookie.setSameSitePolicy(session->d_ptr->cookieSameSite);
608 
609  return cookie;
610 }
611 
612 void SessionPrivate::extendSessionId(Session *session,
613  Context *c,
614  const QByteArray &sid,
615  qint64 expires)
616 {
617  updateSessionCookie(c,
618  makeSessionCookie(session, c, sid, QDateTime::fromSecsSinceEpoch(expires)));
619 }
620 
621 void SessionPrivate::setSessionId(Session *session, Context *c, const QByteArray &sid)
622 {
623  updateSessionCookie(
624  c,
625  makeSessionCookie(
626  session, c, sid, QDateTime::fromSecsSinceEpoch(initialSessionExpires(session, c))));
627 }
628 
629 QVariant SessionPrivate::config(const QString &key, const QVariant &defaultValue) const
630 {
631  return loadedConfig.value(key, defaultConfig.value(key, defaultValue));
632 }
633 
635  : QObject(parent)
636 {
637 }
638 
639 #include "moc_session.cpp"
The Cutelyst application.
Definition: application.h:66
Engine * engine() const noexcept
void afterDispatch(Cutelyst::Context *c)
void postForked(Cutelyst::Application *app)
The Cutelyst Context.
Definition: context.h:42
void stash(const QVariantHash &unite)
Definition: context.cpp:562
Request * request
Definition: context.h:71
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:212
Response * response() const noexcept
Definition: context.cpp:97
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
Base class for Cutelyst Plugins.
Definition: plugin.h:25
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
QHostAddress address() const noexcept
Definition: request.cpp:33
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:212
Abstract class to create a session store.
Definition: session.h:36
SessionStore(QObject *parent=nullptr)
Definition: session.cpp:634
Plugin providing methods for session management.
Definition: session.h:161
static void deleteSession(Context *c, const QString &reason=QString())
Definition: session.cpp:154
static QString deleteReason(Context *c)
Definition: session.cpp:163
virtual bool setup(Application *app) final
Definition: session.cpp:53
Session(Application *parent)
Definition: session.cpp:35
static bool isValid(Context *c)
Definition: session.cpp:257
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:168
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:183
void setStorage(std::unique_ptr< SessionStore > store)
Definition: session.cpp:89
static void changeExpires(Context *c, quint64 expires)
Definition: session.cpp:141
static QByteArray id(Context *c)
Definition: session.cpp:103
SessionStore * storage() const
Definition: session.cpp:97
static void deleteValue(Context *c, const QString &key)
Definition: session.cpp:207
static quint64 expires(Context *c)
Definition: session.cpp:121
virtual ~Session()
Definition: session.cpp:48
static void deleteValues(Context *c, const QStringList &keys)
Definition: session.cpp:231
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
The Cutelyst namespace holds all public Cutelyst API.
bool isEmpty() const const
qsizetype size() const const
QByteArray toHex(char separator) const const
QDateTime currentDateTimeUtc()
qint64 currentSecsSinceEpoch()
QDateTime fromSecsSinceEpoch(qint64 secs)
QString toString() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
CaseInsensitive
QUuid createUuid()
QByteArray toRfc4122() const const
bool isNull() const const
QByteArray toByteArray() const const
QHash< QString, QVariant > toHash() const const
qlonglong toLongLong(bool *ok) const const
T value() const const