cutelyst  4.8.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
context.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2013-2022 Daniel Nicoletti <dantti12@gmail.com>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 #include "action.h"
6 #include "application.h"
7 #include "common.h"
8 #include "config.h"
9 #include "context_p.h"
10 #include "controller.h"
11 #include "dispatcher.h"
12 #include "enginerequest.h"
13 #include "request.h"
14 #include "response.h"
15 #include "stats.h"
16 
17 #include <QBuffer>
18 #include <QCoreApplication>
19 #include <QUrl>
20 #include <QUrlQuery>
21 
22 using namespace Cutelyst;
23 using namespace Qt::Literals::StringLiterals;
24 
25 Context::Context(ContextPrivate *priv)
26  : d_ptr(priv)
27 {
28 }
29 
31  : d_ptr(new ContextPrivate(app, app->engine(), app->dispatcher(), app->plugins()))
32 {
33  auto req = new DummyRequest(this);
34  req->body = new QBuffer(this);
36  req->context = this;
37 
38  d_ptr->response = new Response(app->defaultHeaders(), req);
39  d_ptr->request = new Request(req);
40  d_ptr->request->d_ptr->engine = d_ptr->engine;
41  d_ptr->locale = app->defaultLocale();
42 }
43 
45 {
46  delete d_ptr->request;
47  delete d_ptr->response;
48  delete d_ptr;
49 }
50 
51 bool Context::error() const noexcept
52 {
53  Q_D(const Context);
54  return !d->error.isEmpty();
55 }
56 
57 void Context::appendError(const QString &error)
58 {
59  Q_D(Context);
60  if (error.isEmpty()) {
61  d->error.clear();
62  } else {
63  d->error << error;
64  qCCritical(CUTELYST_CORE) << error;
65  }
66 }
67 
68 QStringList Context::errors() const noexcept
69 {
70  Q_D(const Context);
71  return d->error;
72 }
73 
74 bool Context::state() const noexcept
75 {
76  Q_D(const Context);
77  return d->state;
78 }
79 
80 void Context::setState(bool state) noexcept
81 {
82  Q_D(Context);
83  d->state = state;
84 }
85 
86 Engine *Context::engine() const noexcept
87 {
88  Q_D(const Context);
89  return d->engine;
90 }
91 
92 Application *Context::app() const noexcept
93 {
94  Q_D(const Context);
95  return d->app;
96 }
97 
98 Response *Context::response() const noexcept
99 {
100  Q_D(const Context);
101  return d->response;
102 }
103 
104 Response *Context::res() const noexcept
105 {
106  Q_D(const Context);
107  return d->response;
108 }
109 
110 Action *Context::action() const noexcept
111 {
112  Q_D(const Context);
113  return d->action;
114 }
115 
116 QString Context::actionName() const noexcept
117 {
118  Q_D(const Context);
119  return d->action->name();
120 }
121 
122 QString Context::ns() const noexcept
123 {
124  Q_D(const Context);
125  return d->action->ns();
126 }
127 
128 Request *Context::request() const noexcept
129 {
130  Q_D(const Context);
131  return d->request;
132 }
133 
134 Request *Context::req() const noexcept
135 {
136  Q_D(const Context);
137  return d->request;
138 }
139 
141 {
142  Q_D(const Context);
143  return d->dispatcher;
144 }
145 
147 {
148  Q_D(const Context);
149  return d->action->className();
150 }
151 
152 Controller *Context::controller() const noexcept
153 {
154  Q_D(const Context);
155  return d->action->controller();
156 }
157 
159 {
160  Q_D(const Context);
161  return d->dispatcher->controller(name);
162 }
163 
164 View *Context::customView() const noexcept
165 {
166  Q_D(const Context);
167  return d->view;
168 }
169 
171 {
172  Q_D(const Context);
173  return d->app->view(name);
174 }
175 
177 {
178  Q_D(Context);
179  d->view = d->app->view(name);
180  return d->view;
181 }
182 
183 QVariantHash &Context::stash()
184 {
185  Q_D(Context);
186  return d->stash;
187 }
188 
189 QVariant Context::stash(const QString &key) const
190 {
191  Q_D(const Context);
192  return d->stash.value(key);
193 }
194 
195 QVariant Context::stash(const QString &key, const QVariant &defaultValue) const
196 {
197  Q_D(const Context);
198  return d->stash.value(key, defaultValue);
199 }
200 
202 {
203  Q_D(Context);
204  return d->stash.take(key);
205 }
206 
208 {
209  Q_D(Context);
210  return d->stash.remove(key);
211 }
212 
213 void Context::setStash(const QString &key, const QVariant &value)
214 {
215  Q_D(Context);
216  d->stash.insert(key, value);
217 }
218 
219 void Context::setStash(const QString &key, const ParamsMultiMap &map)
220 {
221  Q_D(Context);
222  d->stash.insert(key, QVariant::fromValue(map));
223 }
224 
226 {
227  Q_D(const Context);
228  return d->stack;
229 }
230 
232  const QStringList &args,
233  const ParamsMultiMap &queryValues) const
234 {
235  Q_D(const Context);
236 
237  QUrl uri = d->request->uri();
238 
239  QString _path;
240  if (path.isEmpty()) {
241  // ns must NOT return a leading slash
242  const QString controllerNS = d->action->controller()->ns();
243  if (!controllerNS.isEmpty()) {
244  _path.prepend(controllerNS);
245  }
246  } else {
247  _path = path;
248  }
249 
250  if (!args.isEmpty()) {
251  if (_path.compare(u"/") == 0) {
252  _path += args.join(u'/');
253  } else {
254  _path = _path + u'/' + args.join(u'/');
255  }
256  }
257 
258  if (!_path.startsWith(u'/')) {
259  _path.prepend(u'/');
260  }
261  uri.setPath(_path, QUrl::DecodedMode);
262 
263  QUrlQuery query;
264  if (!queryValues.isEmpty()) {
265  // Avoid a trailing '?'
266  if (queryValues.size()) {
267  auto it = queryValues.constEnd();
268  while (it != queryValues.constBegin()) {
269  --it;
270  query.addQueryItem(it.key(), it.value());
271  }
272  }
273  }
274  uri.setQuery(query);
275 
276  return uri;
277 }
278 
280  const QStringList &captures,
281  const QStringList &args,
282  const ParamsMultiMap &queryValues) const
283 {
284  Q_D(const Context);
285 
286  QUrl uri;
287  if (action == nullptr) {
288  action = d->action;
289  }
290 
291  QStringList localArgs = args;
292  QStringList localCaptures = captures;
293 
294  Action *expandedAction = d->dispatcher->expandAction(this, action);
295  if (expandedAction->numberOfCaptures() > 0) {
296  while (localCaptures.size() < expandedAction->numberOfCaptures() && localArgs.size()) {
297  localCaptures.append(localArgs.takeFirst());
298  }
299  } else {
300  QStringList localCapturesAux = localCaptures;
301  localCapturesAux.append(localArgs);
302  localArgs = localCapturesAux;
303  localCaptures = QStringList();
304  }
305 
306  const QString path = d->dispatcher->uriForAction(action, localCaptures);
307  if (path.isEmpty()) {
308  qCWarning(CUTELYST_CORE) << "Can not find action for" << action << localCaptures;
309  return uri;
310  }
311 
312  uri = uriFor(path, localArgs, queryValues);
313  return uri;
314 }
315 
317  const QStringList &captures,
318  const QStringList &args,
319  const ParamsMultiMap &queryValues) const
320 {
321  Q_D(const Context);
322 
323  QUrl uri;
324  Action *action = d->dispatcher->getActionByPath(path);
325  if (!action) {
326  qCWarning(CUTELYST_CORE) << "Can not find action for" << path;
327  return uri;
328  }
329 
330  uri = uriFor(action, captures, args, queryValues);
331  return uri;
332 }
333 
334 bool Context::detached() const noexcept
335 {
336  Q_D(const Context);
337  return d->detached;
338 }
339 
340 void Context::detach(Action *action)
341 {
342  Q_D(Context);
343  if (action) {
344  d->dispatcher->forward(this, action);
345  } else {
346  d->detached = true;
347  }
348 }
349 
350 void Context::detachAsync() noexcept
351 {
352  Q_D(Context);
353  ++d->actionRefCount;
354 }
355 
357 {
358  Q_D(Context);
359 
360  // ASync might be destroyed at the same stack level it was created
361  // resulting in this method being called while it was caller,
362  // allowing this method to call finished() twice, with
363  // a null context the second time so we check also check
364  // if the action stack is not empty to skip this method
365  if (--d->actionRefCount || !d->stack.isEmpty()) {
366  return;
367  }
368 
369  if (Q_UNLIKELY(d->engineRequest->status & EngineRequest::Finalized)) {
370  qCWarning(CUTELYST_ASYNC) << "Trying to async attach to a finalized request! Skipping...";
371  return;
372  }
373 
374  if (d->engineRequest->status & EngineRequest::Async) {
375  while (!d->pendingAsync.isEmpty()) {
376  Component *action = d->pendingAsync.dequeue();
377  const bool ret = execute(action);
378 
379  if (d->actionRefCount) {
380  return;
381  }
382 
383  if (!ret) {
384  break; // we are finished
385  }
386  }
387 
388  Q_EMIT d->app->afterDispatch(this);
389 
390  finalize();
391  }
392 }
393 
395 {
396  Q_D(Context);
397  return d->dispatcher->forward(this, action);
398 }
399 
401 {
402  Q_D(Context);
403  return d->dispatcher->forward(this, action);
404 }
405 
407 {
408  Q_D(const Context);
409  return d->dispatcher->getAction(action, ns);
410 }
411 
413 {
414  Q_D(const Context);
415  return d->dispatcher->getActions(action, ns);
416 }
417 
419 {
420  Q_D(const Context);
421  return d->plugins;
422 }
423 
425 {
426  Q_D(Context);
427  Q_ASSERT_X(code, "Context::execute", "trying to execute a null Cutelyst::Component");
428 
429  static int recursion =
430  qEnvironmentVariableIsSet("RECURSION") ? qEnvironmentVariableIntValue("RECURSION") : 1000;
431  if (d->stack.size() >= recursion) {
432  QString msg = QStringLiteral("Deep recursion detected (stack size %1) calling %2, %3")
433  .arg(QString::number(d->stack.size()), code->reverse(), code->name());
434  appendError(msg);
435  setState(false);
436  return false;
437  }
438 
439  bool ret;
440  d->stack.push(code);
441 
442  if (d->stats) {
443  const QString statsInfo = d->statsStartExecute(code);
444 
445  ret = code->execute(this);
446 
447  // The request might finalize execution before returning
448  // so it's wise to check for d->stats again
449  if (d->stats && !statsInfo.isEmpty()) {
450  d->statsFinishExecute(statsInfo);
451  }
452  } else {
453  ret = code->execute(this);
454  }
455 
456  d->stack.pop();
457 
458  return ret;
459 }
460 
461 QLocale Context::locale() const noexcept
462 {
463  Q_D(const Context);
464  return d->locale;
465 }
466 
467 void Context::setLocale(const QLocale &locale)
468 {
469  Q_D(Context);
470  d->locale = locale;
471 }
472 
473 QVariant Context::config(const QString &key, const QVariant &defaultValue) const
474 {
475  Q_D(const Context);
476  return d->app->config(key, defaultValue);
477 }
478 
479 QVariantMap Context::config() const noexcept
480 {
481  Q_D(const Context);
482  return d->app->config();
483 }
484 
485 QString Context::translate(const char *context,
486  const char *sourceText,
487  const char *disambiguation,
488  int n) const
489 {
490  Q_D(const Context);
491  return d->app->translate(d->locale, context, sourceText, disambiguation, n);
492 }
493 
495 {
496  Q_D(Context);
497 
498  if (Q_UNLIKELY(d->engineRequest->status & EngineRequest::Finalized)) {
499  qCWarning(CUTELYST_CORE) << "Trying to finalize a finalized request! Skipping...";
500  return;
501  }
502 
503  if (d->stats) {
504  qCDebug(CUTELYST_STATS,
505  "Response Code: %d; Content-Type: %s; Content-Length: %s",
506  d->response->status(),
507  d->response->headers().header("Content-Type"_ba, "unknown"_ba).constData(),
508  d->response->headers()
509  .header("Content-Length"_ba, QByteArray::number(d->response->size()))
510  .constData());
511 
512  const std::chrono::duration<double> duration =
513  std::chrono::steady_clock::now() - d->engineRequest->startOfRequest;
514 
515  QString average;
516  if (duration.count() == 0.0) {
517  average = QStringLiteral("??");
518  } else {
519  average = QString::number(1.0 / duration.count(), 'f');
520  average.truncate(average.size() - 3);
521  }
522  qCInfo(CUTELYST_STATS) << qPrintable(QStringLiteral("Request took: %1s (%2/s)\n%3")
523  .arg(QString::number(duration.count(), 'f'),
524  average,
525  QString::fromLatin1(d->stats->report())));
526  delete d->stats;
527  d->stats = nullptr;
528  }
529 
530  d->engineRequest->finalize();
531 }
532 
533 QString ContextPrivate::statsStartExecute(Component *code)
534 {
535  QString actionName;
536  // Skip internal actions
537  if (code->name().startsWith(u'_')) {
538  return actionName;
539  }
540 
541  actionName = code->reverse();
542 
543  if (qobject_cast<Action *>(code)) {
544  actionName.prepend(u'/');
545  }
546 
547  if (stack.size() > 2) {
548  actionName = u"-> " + actionName;
549  actionName =
550  actionName.rightJustified(actionName.size() + stack.size() - 2, QLatin1Char(' '));
551  }
552 
553  stats->profileStart(actionName);
554 
555  return actionName;
556 }
557 
558 void ContextPrivate::statsFinishExecute(const QString &statsInfo)
559 {
560  stats->profileEnd(statsInfo);
561 }
562 
563 void Context::stash(const QVariantHash &unite)
564 {
565  Q_D(Context);
566  auto it = unite.constBegin();
567  while (it != unite.constEnd()) {
568  d->stash.insert(it.key(), it.value());
569  ++it;
570  }
571 }
572 
573 #include "moc_context.cpp"
574 #include "moc_context_p.cpp"
Context(Application *app)
Definition: context.cpp:30
bool execute(Context *c)
Definition: component.cpp:64
void truncate(qsizetype position)
bool state() const noexcept
QString controllerName() const noexcept
bool error() const noexcept
Definition: context.cpp:51
virtual ~Context()
Definition: context.cpp:44
virtual qint8 numberOfCaptures() const
Definition: action.cpp:131
const_iterator constEnd() const const
QString & prepend(QChar ch)
QVariant fromValue(T &&value)
Response * res() const noexcept
Definition: context.cpp:104
bool isEmpty() const const
qsizetype size() const const
size_type size() const const
Action * getAction(QStringView action, QStringView ns={}) const
Definition: context.cpp:406
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:213
QString ns() const noexcept
void detach(Action *action=nullptr)
Definition: context.cpp:340
QUrl uriFor(const QString &path={}, const QStringList &args={}, const ParamsMultiMap &queryValues={}) const
Definition: context.cpp:231
QString join(QChar separator) const const
Request * req() const noexcept
virtual bool open(QIODeviceBase::OpenMode mode)
QString reverse() const noexcept
Definition: component.cpp:45
QIODevice * body() const noexcept
Definition: request.cpp:180
void appendError(const QString &error)
Definition: context.cpp:57
The Cutelyst Component base class.
Definition: component.h:30
QString actionName() const noexcept
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:34
Headers & defaultHeaders() noexcept
Definition: application.cpp:82
A Cutelyst response.
Definition: response.h:28
void setPath(const QString &path, ParsingMode mode)
const_iterator constBegin() const const
void addQueryItem(const QString &key, const QString &value)
The Cutelyst Context.
Definition: context.h:42
bool forward(Component *component)
Definition: context.cpp:394
QString number(double n, char format, int precision)
Cutelyst Controller base class.
Definition: controller.h:55
QByteArray number(double n, char format, int precision)
QLocale defaultLocale() const noexcept
QString rightJustified(qsizetype width, QChar fill, bool truncate) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
Action * action() const noexcept
bool detached() const noexcept
Definition: context.cpp:334
bool isEmpty() const const
bool isEmpty() const const
Request * request() const noexcept
QString translate(const char *context, const char *sourceText, const char *disambiguation=nullptr, int n=-1) const
Definition: context.cpp:485
QVariantMap config() const noexcept
QString name() const noexcept
Definition: component.cpp:33
The Cutelyst namespace holds all public Cutelyst API.
void detachAsync() noexcept
Definition: context.cpp:350
QLocale locale() const noexcept
Definition: context.cpp:461
bool execute(Component *code)
Definition: context.cpp:424
QUrl uriForAction(QStringView path, const QStringList &captures={}, const QStringList &args={}, const ParamsMultiMap &queryValues={}) const
Definition: context.cpp:316
QVector< Plugin * > plugins() const
Definition: context.cpp:418
QVector< Action * > getActions(QStringView action, QStringView ns={}) const
Definition: context.cpp:412
QVariantHash & stash()
Definition: context.cpp:183
void setLocale(const QLocale &locale)
Definition: context.cpp:467
Application * app() const noexcept
Definition: context.cpp:92
QString fromLatin1(QByteArrayView str)
QStack< Component * > stack() const noexcept
Definition: context.cpp:225
QVariant stashTake(const QString &key)
Definition: context.cpp:201
value_type takeFirst()
void append(QList< T > &&value)
Engine * engine() const noexcept
Definition: context.cpp:86
Abstract View component for Cutelyst.
Definition: view.h:24
View * view(QStringView name={}) const
Definition: context.cpp:170
Controller * controller() const noexcept
void setQuery(const QString &query, ParsingMode mode)
bool setCustomView(QStringView name)
Definition: context.cpp:176
The Cutelyst application.
Definition: application.h:72
bool stashRemove(const QString &key)
Definition: context.cpp:207
void setState(bool state) noexcept
Definition: context.cpp:80
A request.
Definition: request.h:41
Response * response() const noexcept
Definition: context.cpp:98
The Cutelyst Engine.
Definition: engine.h:19
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
The Cutelyst Dispatcher.
Definition: dispatcher.h:28
QString arg(Args &&... args) const const
View * customView() const noexcept
Definition: context.cpp:164
QStringList errors() const noexcept
Definition: context.cpp:68
Q_EMITQ_EMIT
void attachAsync()
Definition: context.cpp:356
Dispatcher * dispatcher() const noexcept
Definition: context.cpp:140