cutelyst  5.0.1
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 <ranges>
16 #include <utility>
17 
18 #include <QDir>
19 #include <QFileInfo>
20 #include <QLoggingCategory>
21 #include <QUrl>
22 #include <QUrlQuery>
23 
24 Q_LOGGING_CATEGORY(C_LANGSELECT, "cutelyst.plugin.langselect", QtWarningMsg)
25 
26 using namespace Cutelyst;
27 using namespace Qt::Literals::StringLiterals;
28 
29 const QString LangSelectPrivate::stashKeySelectionTried{u"_c_langselect_tried"_s};
30 
31 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
32 namespace {
33 thread_local LangSelect *lsp = nullptr;
34 } // namespace
35 
36 LangSelect::LangSelect(Application *parent, Cutelyst::LangSelect::Source source)
37  : Plugin(parent)
38  , d_ptr(new LangSelectPrivate)
39 {
40  Q_D(LangSelect);
41  d->source = source;
42  d->autoDetect = true;
43 }
44 
45 LangSelect::LangSelect(Application *parent)
46  : Plugin(parent)
47  , d_ptr(new LangSelectPrivate)
48 {
49  Q_D(LangSelect);
50  d->source = AcceptHeader;
51  d->autoDetect = false;
52 }
53 
54 LangSelect::~LangSelect() = default;
55 
57 {
58  Q_D(LangSelect);
59 
60  const QVariantMap config = app->engine()->config(u"Cutelyst_LangSelect_Plugin"_s);
61 
62  bool cookieExpirationOk = false;
63  const QString cookieExpireStr =
64  config.value(u"cookie_expiration"_s, static_cast<qint64>(d->cookieExpiration.count()))
65  .toString();
66  d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
67  Utils::durationFromString(cookieExpireStr, &cookieExpirationOk));
68  if (!cookieExpirationOk) {
69  qCWarning(C_LANGSELECT).nospace() << "Invalid value set for cookie_expiration. "
70  "Using default value "
71 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
72  << LangSelectPrivate::cookieDefaultExpiration;
73 #else
74  << "1 month";
75 #endif
76  d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
77  }
78 
79  d->cookieDomain = config.value(u"cookie_domain"_s).toString();
80 
81  const QString _sameSite = config.value(u"cookie_same_site"_s, u"lax"_s).toString();
82  if (_sameSite.compare(u"default", Qt::CaseInsensitive) == 0) {
83  d->cookieSameSite = QNetworkCookie::SameSite::Default;
84  } else if (_sameSite.compare(u"none", Qt::CaseInsensitive) == 0) {
85  d->cookieSameSite = QNetworkCookie::SameSite::None;
86  } else if (_sameSite.compare(u"stric", Qt::CaseInsensitive) == 0) {
87  d->cookieSameSite = QNetworkCookie::SameSite::Strict;
88  } else if (_sameSite.compare(u"lax", Qt::CaseInsensitive) == 0) {
89  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
90  } else {
91  qCWarning(C_LANGSELECT).nospace() << "Invalid value set for cookie_same_site. "
92  "Using default value "
93  << QNetworkCookie::SameSite::Lax;
94  d->cookieSameSite = QNetworkCookie::SameSite::Lax;
95  }
96 
97  d->cookieSecure = config.value(u"cookie_secure"_s).toBool();
98 
99  if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
100  qCWarning(C_LANGSELECT) << "cookie_same_site has been set to None but cookie_secure is "
101  "not set to true. Implicitely setting cookie_secure to true. "
102  "Please check your configuration.";
103  d->cookieSecure = true;
104  }
105 
106  if (d->fallbackLocale.language() == QLocale::C) {
107  qCCritical(C_LANGSELECT) << "We need a valid fallback locale.";
108  return false;
109  }
110  if (d->autoDetect) {
111  if (d->source < Fallback) {
112  if (d->source == URLQuery && d->queryKey.isEmpty()) {
113  qCCritical(C_LANGSELECT) << "Can not use url query as source with empty key name.";
114  return false;
115  } else if (d->source == Session && d->sessionKey.isEmpty()) {
116  qCCritical(C_LANGSELECT) << "Can not use session as source with empty key name.";
117  return false;
118  } else if (d->source == Cookie && d->cookieName.isEmpty()) {
119  qCCritical(C_LANGSELECT) << "Can not use cookie as source with empty cookie name.";
120  return false;
121  }
122  } else {
123  qCCritical(C_LANGSELECT) << "Invalid source.";
124  return false;
125  }
126  connect(app, &Application::beforePrepareAction, this, [d](Context *c, bool *skipMethod) {
127  d->beforePrepareAction(c, skipMethod);
128  });
129  }
130  if (!d->locales.contains(d->fallbackLocale)) {
131  d->locales.append(d->fallbackLocale);
132  }
133  connect(app, &Application::postForked, this, &LangSelectPrivate::_q_postFork);
134 
135  qCDebug(C_LANGSELECT) << "Initialized LangSelect plugin with the following settings:";
136  qCDebug(C_LANGSELECT) << "Supported locales:" << d->locales;
137  qCDebug(C_LANGSELECT) << "Fallback locale:" << d->fallbackLocale;
138  qCDebug(C_LANGSELECT) << "Auto detection source:" << d->source;
139  qCDebug(C_LANGSELECT) << "Detect from header:" << d->detectFromHeader;
140 
141  return true;
142 }
143 
144 void LangSelect::setSupportedLocales(const QVector<QLocale> &locales)
145 {
146  Q_D(LangSelect);
147  d->locales.clear();
148  d->locales.reserve(locales.size());
149  for (const QLocale &l : locales) {
150  if (Q_LIKELY(l.language() != QLocale::C)) {
151  d->locales.push_back(l);
152  } else {
153  qCWarning(C_LANGSELECT)
154  << "Can not add invalid locale" << l << "to the list of supported locales.";
155  }
156  }
157 }
158 
159 void LangSelect::setSupportedLocales(const QStringList &locales)
160 {
161  Q_D(LangSelect);
162  d->locales.clear();
163  d->locales.reserve(locales.size());
164  for (const QString &l : locales) {
165  QLocale locale(l);
166  if (Q_LIKELY(locale.language() != QLocale::C)) {
167  d->locales.push_back(locale);
168  } else {
169  qCWarning(C_LANGSELECT)
170  << "Can not add invalid locale" << l << "to the list of supported locales.";
171  }
172  }
173 }
174 
175 void LangSelect::addSupportedLocale(const QLocale &locale)
176 {
177  if (Q_LIKELY(locale.language() != QLocale::C)) {
178  Q_D(LangSelect);
179  d->locales.push_back(locale);
180  } else {
181  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locale
182  << "to the list of supported locales.";
183  }
184 }
185 
186 void LangSelect::addSupportedLocale(const QString &locale)
187 {
188  QLocale l(locale);
189  if (Q_LIKELY(l.language() != QLocale::C)) {
190  Q_D(LangSelect);
191  d->locales.push_back(l);
192  } else {
193  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locale
194  << "to the list of supported locales.";
195  }
196 }
197 
198 void LangSelect::setLocalesFromDir(const QString &path,
199  const QString &name,
200  const QString &prefix,
201  const QString &suffix)
202 {
203  Q_D(LangSelect);
204  d->locales.clear();
205  if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
206  const QDir dir(path);
207  if (Q_LIKELY(dir.exists())) {
208  const auto _pref = prefix.isEmpty() ? u"."_s : prefix;
209  const auto _suff = suffix.isEmpty() ? u".qm"_s : suffix;
210  const QString filter = name + _pref + u'*' + _suff;
211  const auto files = dir.entryInfoList({name}, QDir::Files);
212  if (Q_LIKELY(!files.empty())) {
213  d->locales.reserve(files.size());
214  bool shrinkToFit = false;
215  for (const QFileInfo &fi : files) {
216  const auto fn = fi.fileName();
217  const auto prefIdx = fn.indexOf(_pref);
218  const auto locPart =
219  fn.mid(prefIdx + _pref.length(),
220  fn.length() - prefIdx - _suff.length() - _pref.length());
221  QLocale l(locPart);
222  if (Q_LIKELY(l.language() != QLocale::C)) {
223  d->locales.push_back(l);
224  qCDebug(C_LANGSELECT)
225  << "Added locale" << locPart << "to the list of supported locales.";
226  } else {
227  shrinkToFit = true;
228  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << locPart
229  << "to the list of supported locales.";
230  }
231  }
232  if (shrinkToFit) {
233  d->locales.squeeze();
234  }
235  } else {
236  qCWarning(C_LANGSELECT)
237  << "Can not find translation files for" << filter << "in" << path;
238  }
239  } else {
240  qCWarning(C_LANGSELECT) << "Can not set locales from not existing directory" << path;
241  }
242  } else {
243  qCWarning(C_LANGSELECT) << "Can not set locales from dir with empty path or name.";
244  }
245 }
246 
247 void LangSelect::setLocalesFromDirs(const QString &path, const QString &name)
248 {
249  Q_D(LangSelect);
250  d->locales.clear();
251  if (Q_LIKELY(!path.isEmpty() && !name.isEmpty())) {
252  const QDir dir(path);
253  if (Q_LIKELY(dir.exists())) {
254  const auto dirs = dir.entryList(QDir::AllDirs);
255  if (Q_LIKELY(!dirs.empty())) {
256  d->locales.reserve(dirs.size());
257  bool shrinkToFit = false;
258  for (const QString &subDir : dirs) {
259  const QString relFn = subDir + u'/' + name;
260  if (dir.exists(relFn)) {
261  QLocale l(subDir);
262  if (Q_LIKELY(l.language() != QLocale::C)) {
263  d->locales.push_back(l);
264  qCDebug(C_LANGSELECT)
265  << "Added locale" << subDir << "to the list of supported locales.";
266  } else {
267  shrinkToFit = true;
268  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << subDir
269  << "to the list of supported locales.";
270  }
271  } else {
272  shrinkToFit = true;
273  }
274  }
275  if (shrinkToFit) {
276  d->locales.squeeze();
277  }
278  }
279  } else {
280  qCWarning(C_LANGSELECT) << "Can not set locales from not existing directory" << path;
281  }
282  } else {
283  qCWarning(C_LANGSELECT) << "Can not set locales from dirs with empty path or names.";
284  }
285 }
286 
287 QVector<QLocale> LangSelect::supportedLocales() const
288 {
289  Q_D(const LangSelect);
290  return d->locales;
291 }
292 
293 void LangSelect::setQueryKey(const QString &key)
294 {
295  Q_D(LangSelect);
296  d->queryKey = key;
297 }
298 
299 void LangSelect::setSessionKey(const QString &key)
300 {
301  Q_D(LangSelect);
302  d->sessionKey = key;
303 }
304 
305 void LangSelect::setCookieName(const QByteArray &name)
306 {
307  Q_D(LangSelect);
308  d->cookieName = name;
309 }
310 
311 void LangSelect::setSubDomainMap(const QMap<QString, QLocale> &map)
312 {
313  Q_D(LangSelect);
314  d->subDomainMap.clear();
315  d->locales.clear();
316  d->locales.reserve(map.size());
317  for (const auto &[key, value] : map.asKeyValueRange()) {
318  if (value.language() != QLocale::C) {
319  d->subDomainMap.insert(key, value);
320  d->locales.append(value);
321  } else {
322  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << value << "for subdomain"
323  << key << "to the subdomain map.";
324  }
325  }
326  d->locales.squeeze();
327 }
328 
329 void LangSelect::setDomainMap(const QMap<QString, QLocale> &map)
330 {
331  Q_D(LangSelect);
332  d->domainMap.clear();
333  d->locales.clear();
334  d->locales.reserve(map.size());
335  for (const auto &[key, value] : map.asKeyValueRange()) {
336  if (Q_LIKELY(value.language() != QLocale::C)) {
337  d->domainMap.insert(key, value);
338  d->locales.append(value);
339  } else {
340  qCWarning(C_LANGSELECT) << "Can not add invalid locale" << value << "for domain" << key
341  << "to the domain map.";
342  }
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  for (const auto &[key, value] : map.asKeyValueRange()) {
639  if (domain.startsWith(key)) {
640  qCDebug(C_LANGSELECT) << "Found valid locale" << value << "in subdomain map for domain"
641  << domain;
642  c->setLocale(value);
643  return true;
644  }
645  }
646 
647  const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
648  if (domainParts.size() > 2) {
649  const QLocale l(domainParts.at(0));
650  if (l.language() != QLocale::C && locales.contains(l)) {
651  qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in subdomain of domain"
652  << domain;
653  c->setLocale(l);
654  return true;
655  }
656  }
657  qCDebug(C_LANGSELECT) << "Can not find supported locale for subdomain" << domain;
658  return false;
659 }
660 
661 bool LangSelectPrivate::getFromDomain(Context *c, const QMap<QString, QLocale> &map) const
662 {
663  const auto domain = c->req()->uri().host();
664  for (const auto &[key, value] : map.asKeyValueRange()) {
665  if (domain.endsWith(key)) {
666  qCDebug(C_LANGSELECT) << "Found valid locale" << value << "in domain map for domain"
667  << domain;
668  c->setLocale(value);
669  return true;
670  }
671  }
672 
673  const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
674  if (domainParts.size() > 1) {
675  const QLocale l(domainParts.at(domainParts.size() - 1));
676  if (l.language() != QLocale::C && locales.contains(l)) {
677  qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in domain" << domain;
678  c->setLocale(l);
679  return true;
680  }
681  }
682  qCDebug(C_LANGSELECT) << "Can not find supported locale for domain" << domain;
683  return false;
684 }
685 
686 bool LangSelectPrivate::getFromHeader(Context *c, const QByteArray &name) const
687 {
688  if (detectFromHeader) {
689  const auto accpetedLangs =
690  c->req()->headers().headerAsString(name).split(u',', Qt::SkipEmptyParts);
691  if (Q_LIKELY(!accpetedLangs.empty())) {
692  std::map<float, QLocale> langMap;
693  std::ranges::for_each(accpetedLangs, [&](const auto &al) {
694  const auto idx = al.indexOf(u';');
695  float priority = 1.0F;
696  QString langPart;
697  bool ok = true;
698  if (idx > -1) {
699  langPart = al.left(idx);
700  const auto ref = QStringView(al).mid(idx + 1);
701  priority = ref.mid(ref.indexOf(u'=') + 1).toFloat(&ok);
702  } else {
703  langPart = al;
704  }
705  QLocale locale(langPart);
706  if (ok && locale.language() != QLocale::C) {
707  const auto search = langMap.find(priority);
708  if (search == langMap.cend()) {
709  langMap.insert({priority, locale});
710  }
711  }
712  });
713 
714  if (!langMap.empty()) {
715  // Check for exact locale match in descending priority order
716  auto range = langMap | std::views::reverse;
717  auto found = std::ranges::any_of(range, [&](const auto &entry) {
718  if (locales.contains(entry.second)) {
719  c->setLocale(entry.second);
720  qCDebug(C_LANGSELECT)
721  << "Selected locale" << c->locale() << "from" << name << "header";
722  return true;
723  }
724  return false;
725  });
726  if (found) {
727  return true;
728  }
729 
730  // Fallback to language-only match
731  const auto constLocales = locales;
732  found = std::ranges::any_of(range, [&](const auto &entry) {
733  return std::ranges::any_of(constLocales, [&](const QLocale &l) {
734  if (l.language() == entry.second.language()) {
735  c->setLocale(l);
736  qCDebug(C_LANGSELECT)
737  << "Selected locale" << c->locale() << "from" << name << "header";
738  return true;
739  }
740  return false;
741  });
742  });
743  if (found) {
744  return true;
745  }
746  }
747  }
748  }
749 
750  return false;
751 }
752 
753 void LangSelectPrivate::setToQuery(const Context *c, const QString &key) const
754 {
755  auto uri = c->req()->uri();
756  QUrlQuery query(uri);
757  if (query.hasQueryItem(key)) {
758  query.removeQueryItem(key);
759  }
760  query.addQueryItem(key, c->locale().bcp47Name().toLower());
761  uri.setQuery(query);
762  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in URL query by redirecting to"
763  << uri;
764  c->res()->redirect(uri, Response::TemporaryRedirect);
765 }
766 
767 void LangSelectPrivate::setToCookie(const Context *c, const QByteArray &name) const
768 {
769  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in cookie with name" << name;
770  QNetworkCookie cookie(name, c->locale().bcp47Name().toLatin1());
771  cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
772  if (cookieExpiration.count() == 0) {
773  cookie.setExpirationDate(QDateTime());
774  } else {
775  cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(cookieExpiration));
776  }
777  cookie.setDomain(cookieDomain);
778  cookie.setSecure(cookieSecure);
779  cookie.setSameSitePolicy(cookieSameSite);
780  c->res()->setCookie(cookie);
781 }
782 
783 void LangSelectPrivate::setToSession(Context *c, const QString &key) const
784 {
785  qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in session key" << key;
786  Session::setValue(c, key, c->locale());
787 }
788 
789 void LangSelectPrivate::setFallback(Context *c) const
790 {
791  qCDebug(C_LANGSELECT) << "Can not find fitting locale, using fallback locale" << fallbackLocale;
792  c->setLocale(fallbackLocale);
793 }
794 
795 void LangSelectPrivate::setContentLanguage(Context *c) const
796 {
797  if (addContentLanguageHeader) {
798  c->res()->setHeader("Content-Language"_ba, c->locale().bcp47Name().toLatin1());
799  }
800  c->stash(
801  {{langStashKey, c->locale().bcp47Name()},
802  {dirStashKey, (c->locale().textDirection() == Qt::LeftToRight ? u"ltr"_s : u"rtl"_s)}});
803 }
804 
805 void LangSelectPrivate::beforePrepareAction(Context *c, bool *skipMethod) const
806 {
807  if (*skipMethod) {
808  return;
809  }
810 
811  if (!c->stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
812  return;
813  }
814 
815  detectLocale(c, source, skipMethod);
816 
817  c->setStash(LangSelectPrivate::stashKeySelectionTried, true);
818 }
819 
820 void LangSelectPrivate::_q_postFork(Application *app)
821 {
822  lsp = app->plugin<LangSelect *>();
823 }
824 
825 #include "moc_langselect.cpp"
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:202
auto asKeyValueRange() &
void postForked(Cutelyst::Application *app)
bool empty() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
Response * res() const noexcept
Definition: context.cpp:104
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:338
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:222
void stash(const QVariantHash &unite)
Definition: context.cpp:562
Headers headers() const noexcept
Definition: request.cpp:312
CaseInsensitive
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
Definition: engine.cpp:122
bool isEmpty() const const
Qt::LayoutDirection textDirection() const const
QDateTime currentDateTime()
QString path(ComponentFormattingOptions options) const const
Language language() const const
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:186
QString headerAsString(QAnyStringView key) const
Definition: headers.cpp:427
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:56
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray cookie(QAnyStringView name) const
Definition: request.cpp:277
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:302
QString bcp47Name() const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:171
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
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)