cutelyst  4.4.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
langselect.cpp
1 /*
2  * SPDX-FileCopyrightText: (C) 2018-2022 Matthias Fehring <mf@huessenbergnetz.de>
3  * SPDX-License-Identifier: BSD-3-Clause
4  */
5 
6 #include "langselect_p.h"
7 
8 #include <Cutelyst/Application>
9 #include <Cutelyst/Context>
10 #include <Cutelyst/Engine>
11 #include <Cutelyst/Plugins/Session/Session>
12 #include <Cutelyst/Response>
13 #include <Cutelyst/utils.h>
14 #include <map>
15 #include <utility>
16 
17 #include <QDir>
18 #include <QFileInfo>
19 #include <QLoggingCategory>
20 #include <QUrl>
21 #include <QUrlQuery>
22 
23 Q_LOGGING_CATEGORY(C_LANGSELECT, "cutelyst.plugin.langselect", QtWarningMsg)
24 
25 using namespace Cutelyst;
26 
27 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
28 static thread_local LangSelect *lsp = nullptr;
29 
30 const QString LangSelectPrivate::stashKeySelectionTried{u"_c_langselect_tried"_qs};
31 
32 LangSelect::LangSelect(Application *parent, Cutelyst::LangSelect::Source source)
33  : Plugin(parent)
34  , d_ptr(new LangSelectPrivate)
35 {
36  Q_D(LangSelect);
37  d->source = source;
38  d->autoDetect = true;
39 }
40 
41 LangSelect::LangSelect(Application *parent)
42  : Plugin(parent)
43  , d_ptr(new LangSelectPrivate)
44 {
45  Q_D(LangSelect);
46  d->source = AcceptHeader;
47  d->autoDetect = false;
48 }
49 
50 LangSelect::~LangSelect() = default;
51 
52 bool LangSelect::setup(Application *app)
53 {
54  Q_D(LangSelect);
55 
56  const QVariantMap config = app->engine()->config(u"Cutelyst_LangSelect_Plugin"_qs);
57 
58  bool cookieExpirationOk = false;
59  const QString cookieExpireStr =
60  config.value(u"cookie_expiration"_qs, static_cast<qint64>(d->cookieExpiration.count()))
61  .toString();
62  d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
63  Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
64  if (!cookieExpirationOk) {
65  qCWarning(C_LANGSELECT).nospace() << "Invalid value set for cookie_expiration. "
66  "Using default value "
67 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
68  << LangSelectPrivate::cookieDefaultExpiration;
69 #else
70  << "1 month";
71 #endif
72  d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
73  }
74 
75  d->cookieDomain = config.value(u"cookie_domain"_qs).toString();
76 
77  const QString _sameSite = config.value(u"cookie_same_site"_qs, u"lax"_qs).toString();
78  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
79  d->cookieSameSite = QNetworkCookie::SameSite::Default;
80  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
81  d->cookieSameSite = QNetworkCookie::SameSite::None;
82  } else if (_sameSite.compare(u"stric", Qt::CaseInsensitive) == 0) {
83  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
84  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
85  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
86  } else {
87  qCWarning(C_LANGSELECT).nospace() << "Invalid value set for cookie_same_site. "
88  "Using default value "
89  << QNetworkCookie::SameSite::Lax;
90  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
91  }
92 
93  d->cookieSecure = config.value(u"cookie_secure"_qs).toBool();
94 
95  if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
96  qCWarning(C_LANGSELECT) << "cookie_same_site has been set to None but cookie_secure is "
97  "not set to true. Implicitely setting cookie_secure to true. "
98  "Please check your configuration.";
99  d->cookieSecure = true;
100  }
101 
102  if (d->fallbackLocale.language() == QLocale::C) {
103  qCCritical(C_LANGSELECT) << "We need a valid fallback locale.";
104  return false;
105  }
106  if (d->autoDetect) {
107  if (d->source < Fallback) {
108  if (d->source == URLQuery && d->queryKey.isEmpty()) {
109  qCCritical(C_LANGSELECT) << "Can not use url query as source with empty key name.";
110  return false;
111  } else if (d->source == Session && d->sessionKey.isEmpty()) {
112  qCCritical(C_LANGSELECT) << "Can not use session as source with empty key name.";
113  return false;
114  } else if (d->source == Cookie && d->cookieName.isEmpty()) {
115  qCCritical(C_LANGSELECT) << "Can not use cookie as source with empty cookie name.";
116  return false;
117  }
118  } else {
119  qCCritical(C_LANGSELECT) << "Invalid source.";
120  return false;
121  }
122  connect(app, &Application::beforePrepareAction, this, [d](Context *c, bool *skipMethod) {
123  d->beforePrepareAction(c, skipMethod);
124  });
125  }
126  if (!d->locales.contains(d->fallbackLocale)) {
127  d->locales.append(d->fallbackLocale);
128  }
129  connect(app, &Application::postForked, this, &LangSelectPrivate::_q_postFork);
130 
131  qCDebug(C_LANGSELECT) << "Initialized LangSelect plugin with the following settings:";
132  qCDebug(C_LANGSELECT) << "Supported locales:" << d->locales;
133  qCDebug(C_LANGSELECT) << "Fallback locale:" << d->fallbackLocale;
134  qCDebug(C_LANGSELECT) << "Auto detection source:" << d->source;
135  qCDebug(C_LANGSELECT) << "Detect from header:" << d->detectFromHeader;
136 
137  return true;
138 }
139 
140 void LangSelect::setSupportedLocales(const QVector<QLocale> &locales)
141 {
142  Q_D(LangSelect);
143  d->locales.clear();
144  d->locales.reserve(locales.size());
145  for (const QLocale &l : locales) {
146  if (Q_LIKELY(l.language() != QLocale::C)) {
147  d->locales.push_back(l);
148  } else {
149  qCWarning(C_LANGSELECT)
150  << "Can not add invalid locale" << l << "to the list of supported locales.";
151  }
152  }
153 }
154 
155 void LangSelect::setSupportedLocales(const QStringList &locales)
156 {
157  Q_D(LangSelect);
158  d->locales.clear();
159  d->locales.reserve(locales.size());
160  for (const QString &l : locales) {
161  QLocale locale(l);
162  if (Q_LIKELY(locale.language() != QLocale::C)) {
163  d->locales.push_back(locale);
164  } else {
165  qCWarning(C_LANGSELECT)
166  << "Can not add invalid locale" << l << "to the list of supported locales.";
167  }
168  }
169 }
170 
171 void LangSelect::addSupportedLocale(const QLocale &locale)
172 {
173  if (Q_LIKELY(locale.language() != QLocale::C)) {
174  Q_D(LangSelect);
175  d->locales.push_back(locale);
176  } else {
177  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locale
178  << "to the list of supported locales.";
179  }
180 }
181 
182 void LangSelect::addSupportedLocale(const QString &locale)
183 {
184  QLocale l(locale);
185  if (Q_LIKELY(l.language() != QLocale::C)) {
186  Q_D(LangSelect);
187  d->locales.push_back(l);
188  } else {
189  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locale
190  << "to the list of supported locales.";
191  }
192 }
193 
194 void LangSelect::setLocalesFromDir(const QString &path,
195  const QString &name,
196  const QString &prefix,
197  const QString &suffix)
198 {
199  Q_D(LangSelect);
200  d->locales.clear();
201  if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
202  const QDir dir(path);
203  if (Q_LIKELY(dir.exists())) {
204  const auto _pref = prefix.isEmpty() ? u"."_qs : prefix;
205  const auto _suff = suffix.isEmpty() ? u".qm"_qs : suffix;
206  const QString filter = name + _pref + u'*' + _suff;
207  const auto files = dir.entryInfoList({name}, QDir::Files);
208  if (Q_LIKELY(!files.empty())) {
209  d->locales.reserve(files.size());
210  bool shrinkToFit = false;
211  for (const QFileInfo &fi : files) {
212  const auto fn = fi.fileName();
213  const auto prefIdx = fn.indexOf(_pref);
214  const auto locPart =
215  fn.mid(prefIdx + _pref.length(),
216  fn.length() - prefIdx - _suff.length() - _pref.length());
217  QLocale l(locPart);
218  if (Q_LIKELY(l.language() != QLocale::C)) {
219  d->locales.push_back(l);
220  qCDebug(C_LANGSELECT)
221  << "Added locale" << locPart << "to the list of supported locales.";
222  } else {
223  shrinkToFit = true;
224  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locPart
225  << "to the list of supported locales.";
226  }
227  }
228  if (shrinkToFit) {
229  d->locales.squeeze();
230  }
231  } else {
232  qCWarning(C_LANGSELECT)
233  << "Can not find translation files for" << filter << "in" << path;
234  }
235  } else {
236  qCWarning(C_LANGSELECT) << "Can not set locales from not existing directory" << path;
237  }
238  } else {
239  qCWarning(C_LANGSELECT) << "Can not set locales from dir with empty path or name.";
240  }
241 }
242 
243 void LangSelect::setLocalesFromDirs(const QString &path, const QString &name)
244 {
245  Q_D(LangSelect);
246  d->locales.clear();
247  if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
248  const QDir dir(path);
249  if (Q_LIKELY(dir.exists())) {
250  const auto dirs = dir.entryList(QDir::AllDirs);
251  if (Q_LIKELY(!dirs.empty())) {
252  d->locales.reserve(dirs.size());
253  bool shrinkToFit = false;
254  for (const QString &subDir : dirs) {
255  const QString relFn = subDir + u'/' + name;
256  if (dir.exists(relFn)) {
257  QLocale l(subDir);
258  if (Q_LIKELY(l.language() != QLocale::C)) {
259  d->locales.push_back(l);
260  qCDebug(C_LANGSELECT)
261  << "Added locale" << subDir << "to the list of supported locales.";
262  } else {
263  shrinkToFit = true;
264  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << subDir
265  << "to the list of supported locales.";
266  }
267  } else {
268  shrinkToFit = true;
269  }
270  }
271  if (shrinkToFit) {
272  d->locales.squeeze();
273  }
274  }
275  } else {
276  qCWarning(C_LANGSELECT) << "Can not set locales from not existing directory" << path;
277  }
278  } else {
279  qCWarning(C_LANGSELECT) << "Can not set locales from dirs with empty path or names.";
280  }
281 }
282 
283 QVector<QLocale> LangSelect::supportedLocales() const
284 {
285  Q_D(const LangSelect);
286  return d->locales;
287 }
288 
289 void LangSelect::setQueryKey(const QString &key)
290 {
291  Q_D(LangSelect);
292  d->queryKey = key;
293 }
294 
295 void LangSelect::setSessionKey(const QString &key)
296 {
297  Q_D(LangSelect);
298  d->sessionKey = key;
299 }
300 
301 void LangSelect::setCookieName(const QByteArray &name)
302 {
303  Q_D(LangSelect);
304  d->cookieName = name;
305 }
306 
307 void LangSelect::setSubDomainMap(const QMap<QString, QLocale> &map)
308 {
309  Q_D(LangSelect);
310  d->subDomainMap.clear();
311  d->locales.clear();
312  d->locales.reserve(map.size());
313  auto i = map.constBegin();
314  while (i != map.constEnd()) {
315  if (i.value().language() != QLocale::C) {
316  d->subDomainMap.insert(i.key(), i.value());
317  d->locales.append(i.value());
318  } else {
319  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << i.value() << "for subdomain"
320  << i.key() << "to the subdomain map.";
321  }
322  ++i;
323  }
324  d->locales.squeeze();
325 }
326 
327 void LangSelect::setDomainMap(const QMap<QString, QLocale> &map)
328 {
329  Q_D(LangSelect);
330  d->domainMap.clear();
331  d->locales.clear();
332  d->locales.reserve(map.size());
333  auto i = map.constBegin();
334  while (i != map.constEnd()) {
335  if (Q_LIKELY(i.value().language() != QLocale::C)) {
336  d->domainMap.insert(i.key(), i.value());
337  d->locales.append(i.value());
338  } else {
339  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << i.value() << "for domain"
340  << i.key() << "to the domain map.";
341  }
342  ++i;
343  }
344  d->locales.squeeze();
345 }
346 
347 void LangSelect::setFallbackLocale(const QLocale &fallback)
348 {
349  Q_D(LangSelect);
350  d->fallbackLocale = fallback;
351 }
352 
353 void LangSelect::setDetectFromHeader(bool enabled)
354 {
355  Q_D(LangSelect);
356  d->detectFromHeader = enabled;
357 }
358 
359 void LangSelect::setLanguageCodeStashKey(const QString &key)
360 {
361  Q_D(LangSelect);
362  if (Q_LIKELY(!key.isEmpty())) {
363  d->langStashKey = key;
364  } else {
365  qCWarning(C_LANGSELECT) << "Can not set an empty key name for the language code stash key. "
366  "Using current key name"
367  << d->langStashKey;
368  }
369 }
370 
371 void LangSelect::setLanguageDirStashKey(const QString &key)
372 {
373  Q_D(LangSelect);
374  if (Q_LIKELY(!key.isEmpty())) {
375  d->dirStashKey = key;
376  } else {
377  qCWarning(C_LANGSELECT) << "Can not set an empty key name for the language direction stash "
378  "key. Using current key name"
379  << d->dirStashKey;
380  }
381 }
382 
383 QVector<QLocale> LangSelect::getSupportedLocales()
384 {
385  if (!lsp) {
386  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
387  return {};
388  }
389 
390  return lsp->supportedLocales();
391 }
392 
393 bool LangSelect::fromUrlQuery(Context *c, const QString &key)
394 {
395  if (!lsp) {
396  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
397  return true;
398  }
399 
400  const auto d = lsp->d_ptr.get();
401  const auto _key = !key.isEmpty() ? key : d->queryKey;
402  if (!d->getFromQuery(c, _key)) {
403  if (!d->getFromHeader(c)) {
404  d->setFallback(c);
405  }
406  d->setToQuery(c, _key);
407  c->detach();
408  return false;
409  }
410  d->setContentLanguage(c);
411 
412  return true;
413 }
414 
415 bool LangSelect::fromSession(Context *c, const QString &key)
416 {
417  bool foundInSession = false;
418 
419  if (!lsp) {
420  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
421  return foundInSession;
422  }
423 
424  const auto d = lsp->d_ptr.get();
425  const auto _key = !key.isEmpty() ? key : d->sessionKey;
426  foundInSession = d->getFromSession(c, _key);
427  if (!foundInSession) {
428  if (!d->getFromHeader(c)) {
429  d->setFallback(c);
430  }
431  d->setToSession(c, _key);
432  }
433  d->setContentLanguage(c);
434 
435  return foundInSession;
436 }
437 
438 bool LangSelect::fromCookie(Context *c, const QByteArray &name)
439 {
440  bool foundInCookie = false;
441 
442  if (!lsp) {
443  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
444  return foundInCookie;
445  }
446 
447  const auto d = lsp->d_ptr.get();
448  const auto _name = !name.isEmpty() ? name : d->cookieName;
449  foundInCookie = d->getFromCookie(c, _name);
450  if (!foundInCookie) {
451  if (!d->getFromHeader(c)) {
452  d->setFallback(c);
453  }
454  d->setToCookie(c, _name);
455  }
456  d->setContentLanguage(c);
457 
458  return foundInCookie;
459 }
460 
461 bool LangSelect::fromSubDomain(Context *c, const QMap<QString, QLocale> &subDomainMap)
462 {
463  bool foundInSubDomain = false;
464 
465  if (!lsp) {
466  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
467  return foundInSubDomain;
468  }
469 
470  const auto d = lsp->d_ptr.get();
471  const auto _map = !subDomainMap.empty() ? subDomainMap : d->subDomainMap;
472  foundInSubDomain = d->getFromSubdomain(c, _map);
473  if (!foundInSubDomain) {
474  if (!d->getFromHeader(c)) {
475  d->setFallback(c);
476  }
477  }
478 
479  d->setContentLanguage(c);
480 
481  return foundInSubDomain;
482 }
483 
484 bool LangSelect::fromDomain(Context *c, const QMap<QString, QLocale> &domainMap)
485 {
486  bool foundInDomain = false;
487 
488  if (!lsp) {
489  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
490  return foundInDomain;
491  }
492 
493  const auto d = lsp->d_ptr.get();
494  const auto _map = !domainMap.empty() ? domainMap : d->domainMap;
495  foundInDomain = d->getFromDomain(c, _map);
496  if (!foundInDomain) {
497  if (!d->getFromHeader(c)) {
498  d->setFallback(c);
499  }
500  }
501 
502  d->setContentLanguage(c);
503 
504  return foundInDomain;
505 }
506 
507 bool LangSelect::fromPath(Context *c, const QString &locale)
508 {
509  if (!lsp) {
510  qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
511  return true;
512  }
513 
514  const auto d = lsp->d_ptr.get();
515  const QLocale l(locale);
516  if (l.language() != QLocale::C && d->locales.contains(l)) {
517  qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in path";
518  c->setLocale(l);
519  d->setContentLanguage(c);
520  return true;
521  } else {
522  if (!d->getFromHeader(c)) {
523  d->setFallback(c);
524  }
525  auto uri = c->req()->uri();
526  auto pathParts = uri.path().split(u'/');
527  const auto localeIdx = pathParts.indexOf(locale);
528  pathParts[localeIdx] = c->locale().bcp47Name().toLower();
529  uri.setPath(pathParts.join(u'/'));
530  qCDebug(C_LANGSELECT) << "Storing selected locale by redirecting to" << uri;
531  c->res()->redirect(uri, Response::TemporaryRedirect);
532  c->detach();
533  return false;
534  }
535 }
536 
537 bool LangSelectPrivate::detectLocale(Context *c, LangSelect::Source _source, bool *skipMethod) const
538 {
539  bool redirect = false;
540 
541  LangSelect::Source foundIn = LangSelect::Fallback;
542 
543  if (_source == LangSelect::Session) {
544  if (getFromSession(c, sessionKey)) {
545  foundIn = _source;
546  }
547  } else if (_source == LangSelect::Cookie) {
548  if (getFromCookie(c, cookieName)) {
549  foundIn = _source;
550  }
551  } else if (_source == LangSelect::URLQuery) {
552  if (getFromQuery(c, queryKey)) {
553  foundIn = _source;
554  }
555  } else if (_source == LangSelect::SubDomain) {
556  if (getFromSubdomain(c, subDomainMap)) {
557  foundIn = _source;
558  }
559  } else if (_source == LangSelect::Domain) {
560  if (getFromDomain(c, domainMap)) {
561  foundIn = _source;
562  }
563  }
564 
565  // could not find supported locale in specified source
566  // falling back to Accept-Language header
567  if (foundIn == LangSelect::Fallback && getFromHeader(c)) {
568  foundIn = LangSelect::AcceptHeader;
569  }
570 
571  if (foundIn == LangSelect::Fallback) {
572  setFallback(c);
573  }
574 
575  if (foundIn != _source) {
576  if (_source == LangSelect::Session) {
577  setToSession(c, sessionKey);
578  } else if (_source == LangSelect::Cookie) {
579  setToCookie(c, cookieName);
580  } else if (_source == LangSelect::URLQuery) {
581  setToQuery(c, queryKey);
582  redirect = true;
583  if (skipMethod) {
584  *skipMethod = true;
585  }
586  }
587  }
588 
589  if (!redirect) {
590  setContentLanguage(c);
591  }
592 
593  return redirect;
594 }
595 
596 bool LangSelectPrivate::getFromQuery(Context *c, const QString &key) const
597 {
598  const QLocale l(c->req()->queryParam(key));
599  if (l.language() != QLocale::C && locales.contains(l)) {
600  qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in url query key" << key;
601  c->setLocale(l);
602  return true;
603  } else {
604  qCDebug(C_LANGSELECT) << "Can not find supported locale in url query key" << key;
605  return false;
606  }
607 }
608 
609 bool LangSelectPrivate::getFromCookie(Context *c, const QByteArray &cookie) const
610 {
611  const QLocale l(QString::fromLatin1(c->req()->cookie(cookie)));
612  if (l.language() != QLocale::C && locales.contains(l)) {
613  qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in cookie name" << cookie;
614  c->setLocale(l);
615  return true;
616  } else {
617  qCDebug(C_LANGSELECT) << "Can no find supported locale in cookie value with name" << cookie;
618  return false;
619  }
620 }
621 
622 bool LangSelectPrivate::getFromSession(Context *c, const QString &key) const
623 {
624  const QLocale l = Cutelyst::Session::value(c, key).toLocale();
625  if (l.language() != QLocale::C && locales.contains(l)) {
626  qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in session key" << key;
627  c->setLocale(l);
628  return true;
629  } else {
630  qCDebug(C_LANGSELECT) << "Can not find supported locale in session value with key" << key;
631  return false;
632  }
633 }
634 
635 bool LangSelectPrivate::getFromSubdomain(Context *c, const QMap<QString, QLocale> &map) const
636 {
637  const auto domain = c->req()->uri().host();
638  auto i = map.constBegin();
639  while (i != map.constEnd()) {
640  if (domain.startsWith(i.key())) {
641  qCDebug(C_LANGSELECT) << "Found valid locale" << i.value()
642  << "in subdomain map for domain" << domain;
643  c->setLocale(i.value());
644  return true;
645  }
646  ++i;
647  }
648 
649  const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
650  if (domainParts.size() > 2) {
651  const QLocale l(domainParts.at(0));
652  if (l.language() != QLocale::C && locales.contains(l)) {
653  qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in subdomain of domain"
654  << domain;
655  c->setLocale(l);
656  return true;
657  }
658  }
659  qCDebug(C_LANGSELECT) << "Can not find supported locale for subdomain" << domain;
660  return false;
661 }
662 
663 bool LangSelectPrivate::getFromDomain(Context *c, const QMap<QString, QLocale> &map) const
664 {
665  const auto domain = c->req()->uri().host();
666  auto i = map.constBegin();
667  while (i != map.constEnd()) {
668  if (domain.endsWith(i.key())) {
669  qCDebug(C_LANGSELECT) << "Found valid locale" << i.value() << "in domain map for domain"
670  << domain;
671  c->setLocale(i.value());
672  return true;
673  }
674  ++i;
675  }
676 
677  const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
678  if (domainParts.size() > 1) {
679  const QLocale l(domainParts.at(domainParts.size() - 1));
680  if (l.language() != QLocale::C && locales.contains(l)) {
681  qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in domain" << domain;
682  c->setLocale(l);
683  return true;
684  }
685  }
686  qCDebug(C_LANGSELECT) << "Can not find supported locale for domain" << domain;
687  return false;
688 }
689 
690 bool LangSelectPrivate::getFromHeader(Context *c, const QByteArray &name) const
691 {
692  if (detectFromHeader) {
693  // TODO Qt::SkipEmptyParts
694  const auto accpetedLangs = c->req()->header(name).split(',');
695  if (Q_LIKELY(!accpetedLangs.empty())) {
696  std::map<float, QLocale> langMap;
697  for (const auto &ba : accpetedLangs) {
698  const QString al = QString::fromLatin1(ba);
699  const auto idx = al.indexOf(u';');
700  float priority = 1.0f;
701  QString langPart;
702  bool ok = true;
703  if (idx > -1) {
704  langPart = al.left(idx);
705  const auto ref = QStringView(al).mid(idx + 1);
706  priority = ref.mid(ref.indexOf(u'=') + 1).toFloat(&ok);
707  } else {
708  langPart = al;
709  }
710  QLocale locale(langPart);
711  if (ok && locale.language() != QLocale::C) {
712  const auto search = langMap.find(priority);
713  if (search == langMap.cend()) {
714  langMap.insert({priority, locale});
715  }
716  }
717  }
718  if (!langMap.empty()) {
719  auto i = langMap.crbegin();
720  while (i != langMap.crend()) {
721  if (locales.contains(i->second)) {
722  c->setLocale(i->second);
723  qCDebug(C_LANGSELECT)
724  << "Selected locale" << c->locale() << "from" << name << "header";
725  return true;
726  }
727  ++i;
728  }
729  // if there is no exact match, lets try to find a locale
730  // where at least the language matches
731  i = langMap.crbegin();
732  const auto constLocales = locales;
733  while (i != langMap.crend()) {
734  for (const QLocale &l : constLocales) {
735  if (l.language() == i->second.language()) {
736  c->setLocale(l);
737  qCDebug(C_LANGSELECT)
738  << "Selected locale" << c->locale() << "from" << name << "header";
739  return true;
740  }
741  }
742  ++i;
743  }
744  }
745  }
746  }
747 
748  return false;
749 }
750 
751 void LangSelectPrivate::setToQuery(Context *c, const QString &key) const
752 {
753  auto uri = c->req()->uri();
754  QUrlQuery query(uri);
755  if (query.hasQueryItem(key)) {
756  query.removeQueryItem(key);
757  }
758  query.addQueryItem(key, c->locale().bcp47Name().toLower());
759  uri.setQuery(query);
760  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in URL query by redirecting to"
761  << uri;
762  c->res()->redirect(uri, Response::TemporaryRedirect);
763 }
764 
765 void LangSelectPrivate::setToCookie(Context *c, const QByteArray &name) const
766 {
767  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in cookie with name" << name;
768  QNetworkCookie cookie(name, c->locale().bcp47Name().toLatin1());
769  cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
770  if (cookieExpiration.count() == 0) {
771  cookie.setExpirationDate(QDateTime());
772  } else {
773 #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
774  cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(cookieExpiration));
775 #else
776  cookie.setExpirationDate(QDateTime::currentDateTime().addSecs(cookieExpiration.count()));
777 #endif
778  }
779  cookie.setDomain(cookieDomain);
780  cookie.setSecure(cookieSecure);
781  cookie.setSameSitePolicy(cookieSameSite);
782  c->res()->setCookie(cookie);
783 }
784 
785 void LangSelectPrivate::setToSession(Context *c, const QString &key) const
786 {
787  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in session key" << key;
788  Session::setValue(c, key, c->locale());
789 }
790 
791 void LangSelectPrivate::setFallback(Context *c) const
792 {
793  qCDebug(C_LANGSELECT) << "Can not find fitting locale, using fallback locale" << fallbackLocale;
794  c->setLocale(fallbackLocale);
795 }
796 
797 void LangSelectPrivate::setContentLanguage(Context *c) const
798 {
799  if (addContentLanguageHeader) {
800  c->res()->setHeader("Content-Language"_qba, c->locale().bcp47Name().toLatin1());
801  }
802  c->stash(
803  {{langStashKey, c->locale().bcp47Name()},
804  {dirStashKey, (c->locale().textDirection() == Qt::LeftToRight ? u"ltr"_qs : u"rtl"_qs)}});
805 }
806 
807 void LangSelectPrivate::beforePrepareAction(Context *c, bool *skipMethod) const
808 {
809  if (*skipMethod) {
810  return;
811  }
812 
813  if (!c->stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
814  return;
815  }
816 
817  detectLocale(c, source, skipMethod);
818 
819  c->setStash(LangSelectPrivate::stashKeySelectionTried, true);
820 }
821 
822 void LangSelectPrivate::_q_postFork(Application *app)
823 {
824  lsp = app->plugin<LangSelect *>();
825 }
826 
827 #include "moc_langselect.cpp"
The Cutelyst application.
Definition: application.h:66
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
void postForked(Cutelyst::Application *app)
The Cutelyst Context.
Definition: context.h:42
void stash(const QVariantHash &unite)
Definition: context.cpp:562
void detach(Action *action=nullptr)
Definition: context.cpp:339
QLocale locale() const noexcept
Definition: context.cpp:460
Response * res() const noexcept
Definition: context.cpp:103
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:212
Request * req
Definition: context.h:66
void setLocale(const QLocale &locale)
Definition: context.cpp:466
QVariantMap config(const QString &entity) const
Definition: engine.cpp:263
Detect and select locale based on different input parameters.
Definition: langselect.h:350
Base class for Cutelyst Plugins.
Definition: plugin.h:25
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:277
QString queryParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:591
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:611
void redirect(const QUrl &url, quint16 status=Found)
Definition: response.cpp:232
void setHeader(const QByteArray &key, const QByteArray &value)
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:212
Plugin providing methods for session management.
Definition: session.h:161
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
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
The Cutelyst namespace holds all public Cutelyst API.
QByteArray::const_reverse_iterator crbegin() const const
bool isEmpty() const const
QList< QByteArray > split(char sep) const const
QDateTime currentDateTime()
qsizetype size() const const
QString bcp47Name() const const
QLocale::Language language() const const
const T & value() const const
QMap::const_iterator constBegin() const const
QMap::const_iterator constEnd() const const
bool empty() const const
QMap::size_type size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
void reserve(qsizetype size)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
QString toLower() const const
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
QStringView mid(qsizetype start, qsizetype length) const const
float toFloat(bool *ok) const const
CaseInsensitive
LeftToRight
SkipEmptyParts
QString host(QUrl::ComponentFormattingOptions options) const const
QString path(QUrl::ComponentFormattingOptions options) const const
QLocale toLocale() const const