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