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