cutelyst 4.0.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#if defined(QT_DEBUG)
24Q_LOGGING_CATEGORY(C_LANGSELECT, "cutelyst.plugin.langselect")
25#else
26Q_LOGGING_CATEGORY(C_LANGSELECT, "cutelyst.plugin.langselect", QtWarningMsg)
27#endif
28
29using namespace Cutelyst;
30
31// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
32static thread_local LangSelect *lsp = nullptr;
33
34const QString LangSelectPrivate::stashKeySelectionTried{u"_c_langselect_tried"_qs};
35
37 : Plugin(parent)
38 , d_ptr(new LangSelectPrivate)
39{
40 Q_D(LangSelect);
41 d->source = source;
42 d->autoDetect = true;
43}
44
46 : Plugin(parent)
47 , d_ptr(new LangSelectPrivate)
48{
49 Q_D(LangSelect);
50 d->source = AcceptHeader;
51 d->autoDetect = false;
52}
53
54LangSelect::~LangSelect() = default;
55
57{
58 Q_D(LangSelect);
59
60 const QVariantMap config = app->engine()->config(u"Cutelyst_LangSelect_Plugin"_qs);
61
62 bool cookieExpirationOk = false;
63 const QString cookieExpireStr =
64 config.value(u"cookie_expiration"_qs, 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"_qs).toString();
80
81 const QString _sameSite = config.value(u"cookie_same_site"_qs, u"lax"_qs).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"_qs).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
144void 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
159void 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
175void 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
186void 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
198void 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"."_qs : prefix;
209 const auto _suff = suffix.isEmpty() ? u".qm"_qs : 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
247void 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
287QVector<QLocale> LangSelect::supportedLocales() const
288{
289 Q_D(const LangSelect);
290 return d->locales;
291}
292
293void LangSelect::setQueryKey(const QString &key)
294{
295 Q_D(LangSelect);
296 d->queryKey = key;
297}
298
299void LangSelect::setSessionKey(const QString &key)
300{
301 Q_D(LangSelect);
302 d->sessionKey = key;
303}
304
305void LangSelect::setCookieName(const QByteArray &name)
306{
307 Q_D(LangSelect);
308 d->cookieName = name;
309}
310
311void 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 auto i = map.constBegin();
318 while (i != map.constEnd()) {
319 if (i.value().language() != QLocale::C) {
320 d->subDomainMap.insert(i.key(), i.value());
321 d->locales.append(i.value());
322 } else {
323 qCWarning(C_LANGSELECT) << "Can not add invalid locale" << i.value() << "for subdomain"
324 << i.key() << "to the subdomain map.";
325 }
326 ++i;
327 }
328 d->locales.squeeze();
329}
330
331void LangSelect::setDomainMap(const QMap<QString, QLocale> &map)
332{
333 Q_D(LangSelect);
334 d->domainMap.clear();
335 d->locales.clear();
336 d->locales.reserve(map.size());
337 auto i = map.constBegin();
338 while (i != map.constEnd()) {
339 if (Q_LIKELY(i.value().language() != QLocale::C)) {
340 d->domainMap.insert(i.key(), i.value());
341 d->locales.append(i.value());
342 } else {
343 qCWarning(C_LANGSELECT) << "Can not add invalid locale" << i.value() << "for domain"
344 << i.key() << "to the domain map.";
345 }
346 ++i;
347 }
348 d->locales.squeeze();
349}
350
351void LangSelect::setFallbackLocale(const QLocale &fallback)
352{
353 Q_D(LangSelect);
354 d->fallbackLocale = fallback;
355}
356
358{
359 Q_D(LangSelect);
360 d->detectFromHeader = enabled;
361}
362
364{
365 Q_D(LangSelect);
366 if (Q_LIKELY(!key.isEmpty())) {
367 d->langStashKey = key;
368 } else {
369 qCWarning(C_LANGSELECT) << "Can not set an empty key name for the language code stash key. "
370 "Using current key name"
371 << d->langStashKey;
372 }
373}
374
376{
377 Q_D(LangSelect);
378 if (Q_LIKELY(!key.isEmpty())) {
379 d->dirStashKey = key;
380 } else {
381 qCWarning(C_LANGSELECT) << "Can not set an empty key name for the language direction stash "
382 "key. Using current key name"
383 << d->dirStashKey;
384 }
385}
386
388{
389 if (!lsp) {
390 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
391 return {};
392 }
393
394 return lsp->supportedLocales();
395}
396
397bool LangSelect::fromUrlQuery(Context *c, const QString &key)
398{
399 if (!lsp) {
400 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
401 return true;
402 }
403
404 const auto d = lsp->d_ptr.get();
405 const auto _key = !key.isEmpty() ? key : d->queryKey;
406 if (!d->getFromQuery(c, _key)) {
407 if (!d->getFromHeader(c)) {
408 d->setFallback(c);
409 }
410 d->setToQuery(c, _key);
411 c->detach();
412 return false;
413 }
414 d->setContentLanguage(c);
415
416 return true;
417}
418
419bool LangSelect::fromSession(Context *c, const QString &key)
420{
421 bool foundInSession = false;
422
423 if (!lsp) {
424 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
425 return foundInSession;
426 }
427
428 const auto d = lsp->d_ptr.get();
429 const auto _key = !key.isEmpty() ? key : d->sessionKey;
430 foundInSession = d->getFromSession(c, _key);
431 if (!foundInSession) {
432 if (!d->getFromHeader(c)) {
433 d->setFallback(c);
434 }
435 d->setToSession(c, _key);
436 }
437 d->setContentLanguage(c);
438
439 return foundInSession;
440}
441
442bool LangSelect::fromCookie(Context *c, const QByteArray &name)
443{
444 bool foundInCookie = false;
445
446 if (!lsp) {
447 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
448 return foundInCookie;
449 }
450
451 const auto d = lsp->d_ptr.get();
452 const auto _name = !name.isEmpty() ? name : d->cookieName;
453 foundInCookie = d->getFromCookie(c, _name);
454 if (!foundInCookie) {
455 if (!d->getFromHeader(c)) {
456 d->setFallback(c);
457 }
458 d->setToCookie(c, _name);
459 }
460 d->setContentLanguage(c);
461
462 return foundInCookie;
463}
464
465bool LangSelect::fromSubDomain(Context *c, const QMap<QString, QLocale> &subDomainMap)
466{
467 bool foundInSubDomain = false;
468
469 if (!lsp) {
470 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
471 return foundInSubDomain;
472 }
473
474 const auto d = lsp->d_ptr.get();
475 const auto _map = !subDomainMap.empty() ? subDomainMap : d->subDomainMap;
476 foundInSubDomain = d->getFromSubdomain(c, _map);
477 if (!foundInSubDomain) {
478 if (!d->getFromHeader(c)) {
479 d->setFallback(c);
480 }
481 }
482
483 d->setContentLanguage(c);
484
485 return foundInSubDomain;
486}
487
488bool LangSelect::fromDomain(Context *c, const QMap<QString, QLocale> &domainMap)
489{
490 bool foundInDomain = false;
491
492 if (!lsp) {
493 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
494 return foundInDomain;
495 }
496
497 const auto d = lsp->d_ptr.get();
498 const auto _map = !domainMap.empty() ? domainMap : d->domainMap;
499 foundInDomain = d->getFromDomain(c, _map);
500 if (!foundInDomain) {
501 if (!d->getFromHeader(c)) {
502 d->setFallback(c);
503 }
504 }
505
506 d->setContentLanguage(c);
507
508 return foundInDomain;
509}
510
511bool LangSelect::fromPath(Context *c, const QString &locale)
512{
513 if (!lsp) {
514 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
515 return true;
516 }
517
518 const auto d = lsp->d_ptr.get();
519 const QLocale l(locale);
520 if (l.language() != QLocale::C && d->locales.contains(l)) {
521 qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in path";
522 c->setLocale(l);
523 d->setContentLanguage(c);
524 return true;
525 } else {
526 if (!d->getFromHeader(c)) {
527 d->setFallback(c);
528 }
529 auto uri = c->req()->uri();
530 auto pathParts = uri.path().split(u'/');
531 const auto localeIdx = pathParts.indexOf(locale);
532 pathParts[localeIdx] = c->locale().bcp47Name().toLower();
533 uri.setPath(pathParts.join(u'/'));
534 qCDebug(C_LANGSELECT) << "Storing selected locale by redirecting to" << uri;
535 c->res()->redirect(uri, Response::TemporaryRedirect);
536 c->detach();
537 return false;
538 }
539}
540
541bool LangSelectPrivate::detectLocale(Context *c, LangSelect::Source _source, bool *skipMethod) const
542{
543 bool redirect = false;
544
546
547 if (_source == LangSelect::Session) {
548 if (getFromSession(c, sessionKey)) {
549 foundIn = _source;
550 }
551 } else if (_source == LangSelect::Cookie) {
552 if (getFromCookie(c, cookieName)) {
553 foundIn = _source;
554 }
555 } else if (_source == LangSelect::URLQuery) {
556 if (getFromQuery(c, queryKey)) {
557 foundIn = _source;
558 }
559 } else if (_source == LangSelect::SubDomain) {
560 if (getFromSubdomain(c, subDomainMap)) {
561 foundIn = _source;
562 }
563 } else if (_source == LangSelect::Domain) {
564 if (getFromDomain(c, domainMap)) {
565 foundIn = _source;
566 }
567 }
568
569 // could not find supported locale in specified source
570 // falling back to Accept-Language header
571 if (foundIn == LangSelect::Fallback && getFromHeader(c)) {
572 foundIn = LangSelect::AcceptHeader;
573 }
574
575 if (foundIn == LangSelect::Fallback) {
576 setFallback(c);
577 }
578
579 if (foundIn != _source) {
580 if (_source == LangSelect::Session) {
581 setToSession(c, sessionKey);
582 } else if (_source == LangSelect::Cookie) {
583 setToCookie(c, cookieName);
584 } else if (_source == LangSelect::URLQuery) {
585 setToQuery(c, queryKey);
586 redirect = true;
587 if (skipMethod) {
588 *skipMethod = true;
589 }
590 }
591 }
592
593 if (!redirect) {
594 setContentLanguage(c);
595 }
596
597 return redirect;
598}
599
600bool LangSelectPrivate::getFromQuery(Context *c, const QString &key) const
601{
602 const QLocale l(c->req()->queryParam(key));
603 if (l.language() != QLocale::C && locales.contains(l)) {
604 qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in url query key" << key;
605 c->setLocale(l);
606 return true;
607 } else {
608 qCDebug(C_LANGSELECT) << "Can not find supported locale in url query key" << key;
609 return false;
610 }
611}
612
613bool LangSelectPrivate::getFromCookie(Context *c, const QByteArray &cookie) const
614{
615 const QLocale l(QString::fromLatin1(c->req()->cookie(cookie)));
616 if (l.language() != QLocale::C && locales.contains(l)) {
617 qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in cookie name" << cookie;
618 c->setLocale(l);
619 return true;
620 } else {
621 qCDebug(C_LANGSELECT) << "Can no find supported locale in cookie value with name" << cookie;
622 return false;
623 }
624}
625
626bool LangSelectPrivate::getFromSession(Context *c, const QString &key) const
627{
628 const QLocale l = Cutelyst::Session::value(c, key).toLocale();
629 if (l.language() != QLocale::C && locales.contains(l)) {
630 qCDebug(C_LANGSELECT) << "Found valid locale" << l << "in session key" << key;
631 c->setLocale(l);
632 return true;
633 } else {
634 qCDebug(C_LANGSELECT) << "Can not find supported locale in session value with key" << key;
635 return false;
636 }
637}
638
639bool LangSelectPrivate::getFromSubdomain(Context *c, const QMap<QString, QLocale> &map) const
640{
641 const auto domain = c->req()->uri().host();
642 auto i = map.constBegin();
643 while (i != map.constEnd()) {
644 if (domain.startsWith(i.key())) {
645 qCDebug(C_LANGSELECT) << "Found valid locale" << i.value()
646 << "in subdomain map for domain" << domain;
647 c->setLocale(i.value());
648 return true;
649 }
650 ++i;
651 }
652
653 const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
654 if (domainParts.size() > 2) {
655 const QLocale l(domainParts.at(0));
656 if (l.language() != QLocale::C && locales.contains(l)) {
657 qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in subdomain of domain"
658 << domain;
659 c->setLocale(l);
660 return true;
661 }
662 }
663 qCDebug(C_LANGSELECT) << "Can not find supported locale for subdomain" << domain;
664 return false;
665}
666
667bool LangSelectPrivate::getFromDomain(Context *c, const QMap<QString, QLocale> &map) const
668{
669 const auto domain = c->req()->uri().host();
670 auto i = map.constBegin();
671 while (i != map.constEnd()) {
672 if (domain.endsWith(i.key())) {
673 qCDebug(C_LANGSELECT) << "Found valid locale" << i.value() << "in domain map for domain"
674 << domain;
675 c->setLocale(i.value());
676 return true;
677 }
678 ++i;
679 }
680
681 const auto domainParts = domain.split(u'.', Qt::SkipEmptyParts);
682 if (domainParts.size() > 1) {
683 const QLocale l(domainParts.at(domainParts.size() - 1));
684 if (l.language() != QLocale::C && locales.contains(l)) {
685 qCDebug(C_LANGSELECT) << "Found supported locale" << l << "in domain" << domain;
686 c->setLocale(l);
687 return true;
688 }
689 }
690 qCDebug(C_LANGSELECT) << "Can not find supported locale for domain" << domain;
691 return false;
692}
693
694bool LangSelectPrivate::getFromHeader(Context *c, const QByteArray &name) const
695{
696 if (detectFromHeader) {
697 // TODO Qt::SkipEmptyParts
698 const auto accpetedLangs = c->req()->header(name).split(',');
699 if (Q_LIKELY(!accpetedLangs.empty())) {
700 std::map<float, QLocale> langMap;
701 for (const auto &ba : accpetedLangs) {
702 const QString al = QString::fromLatin1(ba);
703 const auto idx = al.indexOf(u';');
704 float priority = 1.0f;
705 QString langPart;
706 bool ok = true;
707 if (idx > -1) {
708 langPart = al.left(idx);
709 const auto ref = QStringView(al).mid(idx + 1);
710 priority = ref.mid(ref.indexOf(u'=') + 1).toFloat(&ok);
711 } else {
712 langPart = al;
713 }
714 QLocale locale(langPart);
715 if (ok && locale.language() != QLocale::C) {
716 const auto search = langMap.find(priority);
717 if (search == langMap.cend()) {
718 langMap.insert({priority, locale});
719 }
720 }
721 }
722 if (!langMap.empty()) {
723 auto i = langMap.crbegin();
724 while (i != langMap.crend()) {
725 if (locales.contains(i->second)) {
726 c->setLocale(i->second);
727 qCDebug(C_LANGSELECT)
728 << "Selected locale" << c->locale() << "from" << name << "header";
729 return true;
730 }
731 ++i;
732 }
733 // if there is no exact match, lets try to find a locale
734 // where at least the language matches
735 i = langMap.crbegin();
736 const auto constLocales = locales;
737 while (i != langMap.crend()) {
738 for (const QLocale &l : constLocales) {
739 if (l.language() == i->second.language()) {
740 c->setLocale(l);
741 qCDebug(C_LANGSELECT)
742 << "Selected locale" << c->locale() << "from" << name << "header";
743 return true;
744 }
745 }
746 ++i;
747 }
748 }
749 }
750 }
751
752 return false;
753}
754
755void LangSelectPrivate::setToQuery(Context *c, const QString &key) const
756{
757 auto uri = c->req()->uri();
758 QUrlQuery query(uri);
759 if (query.hasQueryItem(key)) {
760 query.removeQueryItem(key);
761 }
762 query.addQueryItem(key, c->locale().bcp47Name().toLower());
763 uri.setQuery(query);
764 qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in URL query by redirecting to"
765 << uri;
766 c->res()->redirect(uri, Response::TemporaryRedirect);
767}
768
769void LangSelectPrivate::setToCookie(Context *c, const QByteArray &name) const
770{
771 qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in cookie with name" << name;
772 QNetworkCookie cookie(name, c->locale().bcp47Name().toLatin1());
773 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
774 if (cookieExpiration.count() == 0) {
775 cookie.setExpirationDate(QDateTime());
776 } else {
777#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
778 cookie.setExpirationDate(QDateTime::currentDateTime().addDuration(cookieExpiration));
779#else
780 cookie.setExpirationDate(QDateTime::currentDateTime().addSecs(cookieExpiration.count()));
781#endif
782 }
783 cookie.setDomain(cookieDomain);
784 cookie.setSecure(cookieSecure);
785 cookie.setSameSitePolicy(cookieSameSite);
786 c->res()->setCookie(cookie);
787}
788
789void LangSelectPrivate::setToSession(Context *c, const QString &key) const
790{
791 qCDebug(C_LANGSELECT) << "Storing selected" << c->locale() << "in session key" << key;
792 Session::setValue(c, key, c->locale());
793}
794
795void LangSelectPrivate::setFallback(Context *c) const
796{
797 qCDebug(C_LANGSELECT) << "Can not find fitting locale, using fallback locale" << fallbackLocale;
798 c->setLocale(fallbackLocale);
799}
800
801void LangSelectPrivate::setContentLanguage(Context *c) const
802{
803 if (addContentLanguageHeader) {
804 c->res()->setHeader("Content-Language"_qba, c->locale().bcp47Name().toLatin1());
805 }
806 c->stash(
807 {{langStashKey, c->locale().bcp47Name()},
808 {dirStashKey, (c->locale().textDirection() == Qt::LeftToRight ? u"ltr"_qs : u"rtl"_qs)}});
809}
810
811void LangSelectPrivate::beforePrepareAction(Context *c, bool *skipMethod) const
812{
813 if (*skipMethod) {
814 return;
815 }
816
817 if (!c->stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
818 return;
819 }
820
821 detectLocale(c, source, skipMethod);
822
823 c->setStash(LangSelectPrivate::stashKeySelectionTried, true);
824}
825
826void LangSelectPrivate::_q_postFork(Application *app)
827{
828 lsp = app->plugin<LangSelect *>();
829}
830
831#include "moc_langselect.cpp"
The Cutelyst Application.
Definition: application.h:43
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
T plugin()
Returns the registered plugin that casts to the template type T.
Definition: application.h:102
void postForked(Cutelyst::Application *app)
The Cutelyst Context.
Definition: context.h:38
void stash(const QVariantHash &unite)
Definition: context.cpp:553
void detach(Action *action=nullptr)
Definition: context.cpp:338
QLocale locale() const noexcept
Definition: context.cpp:453
Response * res() const noexcept
Definition: context.cpp:102
void setStash(const QString &key, const QVariant &value)
Definition: context.cpp:211
void setLocale(const QLocale &locale)
Definition: context.cpp:459
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:290
Language selection plugin.
Definition: langselect.h:371
void setLocalesFromDir(const QString &path, const QString &name, const QString &prefix=QStringLiteral("."), const QString &suffix=QStringLiteral(".qm"))
Definition: langselect.cpp:198
void setDetectFromHeader(bool enabled)
Definition: langselect.cpp:357
void setLanguageDirStashKey(const QString &key=QStringLiteral("c_langselect_dir"))
Definition: langselect.cpp:375
void setCookieName(const QByteArray &name)
Definition: langselect.cpp:305
static bool fromPath(Context *c, const QString &locale)
Definition: langselect.cpp:511
static QVector< QLocale > getSupportedLocales()
Definition: langselect.cpp:387
void setFallbackLocale(const QLocale &fallback)
Definition: langselect.cpp:351
void setQueryKey(const QString &key)
Definition: langselect.cpp:293
void setSubDomainMap(const QMap< QString, QLocale > &map)
Definition: langselect.cpp:311
static bool fromDomain(Context *c, const QMap< QString, QLocale > &domainMap=QMap< QString, QLocale >())
Definition: langselect.cpp:488
void setDomainMap(const QMap< QString, QLocale > &map)
Definition: langselect.cpp:331
void setLanguageCodeStashKey(const QString &key=QStringLiteral("c_langselect_lang"))
Definition: langselect.cpp:363
static bool fromUrlQuery(Context *c, const QString &key=QString())
Definition: langselect.cpp:397
static bool fromSession(Context *c, const QString &key=QString())
Definition: langselect.cpp:419
void setLocalesFromDirs(const QString &path, const QString &name)
Definition: langselect.cpp:247
static bool fromSubDomain(Context *c, const QMap< QString, QLocale > &subDomainMap=QMap< QString, QLocale >())
Definition: langselect.cpp:465
QVector< QLocale > supportedLocales() const
Definition: langselect.cpp:287
~LangSelect() override
static bool fromCookie(Context *c, const QByteArray &name={})
Definition: langselect.cpp:442
void setSessionKey(const QString &key)
Definition: langselect.cpp:299
void addSupportedLocale(const QLocale &locale)
Definition: langselect.cpp:175
bool setup(Application *app) override
Definition: langselect.cpp:56
void setSupportedLocales(const QVector< QLocale > &locales)
Definition: langselect.cpp:144
LangSelect(Application *parent, Source source)
Definition: langselect.cpp:36
QByteArray cookie(QByteArrayView name) const
Definition: request.cpp:272
QString queryParam(const QString &key, const QString &defaultValue={}) const
Definition: request.h:572
QByteArray header(QByteArrayView key) const noexcept
Definition: request.h:592
void redirect(const QUrl &url, quint16 status=Found)
Definition: response.cpp:217
void setHeader(const QByteArray &key, const QByteArray &value)
void setCookie(const QNetworkCookie &cookie)
Definition: response.cpp:197
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
Definition: session.cpp:158
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition: session.cpp:173
CUTELYST_LIBRARY std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition: utils.cpp:291
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:8