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