cutelyst 5.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 <ranges>
16#include <utility>
17
18#include <QDir>
19#include <QFileInfo>
20#include <QLoggingCategory>
21#include <QUrl>
22#include <QUrlQuery>
23
24Q_LOGGING_CATEGORY(C_LANGSELECT, "cutelyst.plugin.langselect", QtWarningMsg)
25
26using namespace Cutelyst;
27using namespace Qt::Literals::StringLiterals;
28
29const QString LangSelectPrivate::stashKeySelectionTried{u"_c_langselect_tried"_s};
30
31// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
32namespace {
33thread_local LangSelect *lsp = nullptr;
34} // namespace
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"_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
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
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
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
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
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
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
288{
289 Q_D(const LangSelect);
290 return d->locales;
291}
292
294{
295 Q_D(LangSelect);
296 d->queryKey = key;
297}
298
300{
301 Q_D(LangSelect);
302 d->sessionKey = key;
303}
304
306{
307 Q_D(LangSelect);
308 d->cookieName = name;
309}
310
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
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
348{
349 Q_D(LangSelect);
350 d->fallbackLocale = fallback;
351}
352
354{
355 Q_D(LangSelect);
356 d->detectFromHeader = enabled;
357}
358
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
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
384{
385 if (!lsp) {
386 qCCritical(C_LANGSELECT) << "LangSelect plugin not registered";
387 return {};
388 }
389
390 return lsp->supportedLocales();
391}
392
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
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
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
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
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
507bool 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
537bool LangSelectPrivate::detectLocale(Context *c, LangSelect::Source _source, bool *skipMethod) const
538{
539 bool redirect = false;
540
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
596bool 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
609bool 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
622bool 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
635bool 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
661bool 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
686bool LangSelectPrivate::getFromHeader(Context *c, const QByteArray &name) const
687{
688 if (detectFromHeader) {
689 const auto accpetedLangs =
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
753void 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
767void 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
783void 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
789void 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
795void 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
805void 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
820void LangSelectPrivate::_q_postFork(Application *app)
821{
822 lsp = app->plugin<LangSelect *>();
823}
824
825#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:338
QLocale locale() const noexcept
Definition context.cpp:461
Response * res() const noexcept
Definition context.cpp:104
void setStash(const QString &key, const QVariant &value)
Definition context.cpp:213
Request * req
Definition context.h:66
void setLocale(const QLocale &locale)
Definition context.cpp:467
QVariantMap config(const QString &entity) const
Definition engine.cpp:122
QString headerAsString(QAnyStringView key) const
Definition headers.cpp:427
Detect and select locale based on different input parameters.
Definition langselect.h:350
void setLocalesFromDir(const QString &path, const QString &name, const QString &prefix=QStringLiteral("."), const QString &suffix=QStringLiteral(".qm"))
void setDetectFromHeader(bool enabled)
void setLanguageDirStashKey(const QString &key=QStringLiteral("c_langselect_dir"))
void setCookieName(const QByteArray &name)
static bool fromUrlQuery(Context *c, const QString &key={})
static bool fromPath(Context *c, const QString &locale)
static QVector< QLocale > getSupportedLocales()
void setFallbackLocale(const QLocale &fallback)
void setQueryKey(const QString &key)
void setSubDomainMap(const QMap< QString, QLocale > &map)
static bool fromDomain(Context *c, const QMap< QString, QLocale > &domainMap=QMap< QString, QLocale >())
void setDomainMap(const QMap< QString, QLocale > &map)
void setLanguageCodeStashKey(const QString &key=QStringLiteral("c_langselect_lang"))
void setLocalesFromDirs(const QString &path, const QString &name)
static bool fromSubDomain(Context *c, const QMap< QString, QLocale > &subDomainMap=QMap< QString, QLocale >())
QVector< QLocale > supportedLocales() const
~LangSelect() override
static bool fromCookie(Context *c, const QByteArray &name={})
void setSessionKey(const QString &key)
void addSupportedLocale(const QLocale &locale)
bool setup(Application *app) override
static bool fromSession(Context *c, const QString &key={})
void setSupportedLocales(const QVector< QLocale > &locales)
LangSelect(Application *parent, Source source)
Base class for Cutelyst Plugins.
Definition plugin.h:25
Headers headers() const noexcept
Definition request.cpp:312
QString queryParam(const QString &key, const QString &defaultValue={}) const
Definition request.h:591
QByteArray cookie(QAnyStringView name) const
Definition request.cpp:277
void redirect(const QUrl &url, quint16 status=Found)
Definition response.cpp:222
void setHeader(const QByteArray &key, const QByteArray &value)
void setCookie(const QNetworkCookie &cookie)
Definition response.cpp:202
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:171
static void setValue(Context *c, const QString &key, const QVariant &value)
Definition session.cpp:186
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
Definition utils.cpp:302
The Cutelyst namespace holds all public Cutelyst API.
bool isEmpty() const const
QDateTime currentDateTime()
QFileInfoList entryInfoList(QDir::Filters filters, QDir::SortFlags sort) const const
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
bool exists() const const
void reserve(qsizetype size)
qsizetype size() const const
QString bcp47Name() const const
QLocale::Language language() const const
auto asKeyValueRange() &
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)
bool isEmpty() const const
QString left(qsizetype n) const const
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