6#include "langselect_p.h"
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>
20#include <QLoggingCategory>
24Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
29const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_s};
38 , d_ptr(new LangSelectPrivate)
47 , d_ptr(new LangSelectPrivate)
51 d->autoDetect =
false;
60 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_LangSelect_Plugin"_s);
62 bool cookieExpirationOk =
false;
64 config.value(u
"cookie_expiration"_s,
static_cast<qint64
>(d->cookieExpiration.count()))
66 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
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;
76 d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
79 d->cookieDomain = config.value(u
"cookie_domain"_s).toString();
81 const QString _sameSite = config.value(u
"cookie_same_site"_s, u
"lax"_s).toString();
83 d->cookieSameSite = QNetworkCookie::SameSite::Default;
85 d->cookieSameSite = QNetworkCookie::SameSite::None;
87 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
89 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
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;
97 d->cookieSecure = config.value(u
"cookie_secure"_s).toBool();
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;
106 if (d->fallbackLocale.language() ==
QLocale::C) {
107 qCCritical(C_LANGSELECT) <<
"We need a valid fallback locale.";
112 if (d->source ==
URLQuery && d->queryKey.isEmpty()) {
113 qCCritical(C_LANGSELECT) <<
"Can not use url query as source with empty key name.";
115 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
116 qCCritical(C_LANGSELECT) <<
"Can not use session as source with empty key name.";
118 }
else if (d->source ==
Cookie && d->cookieName.isEmpty()) {
119 qCCritical(C_LANGSELECT) <<
"Can not use cookie as source with empty cookie name.";
123 qCCritical(C_LANGSELECT) <<
"Invalid source.";
130 if (!d->locales.contains(d->fallbackLocale)) {
131 d->locales.append(d->fallbackLocale);
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;
148 d->locales.reserve(locales.
size());
149 for (
const QLocale &l : locales) {
151 d->locales.push_back(l);
153 qCWarning(C_LANGSELECT)
154 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
163 d->locales.reserve(locales.
size());
164 for (
const QString &l : locales) {
167 d->locales.push_back(locale);
169 qCWarning(C_LANGSELECT)
170 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
179 d->locales.push_back(locale);
181 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
182 <<
"to the list of supported locales.";
191 d->locales.push_back(l);
193 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
194 <<
"to the list of supported locales.";
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;
212 if (Q_LIKELY(!files.empty())) {
213 d->locales.reserve(files.size());
214 bool shrinkToFit =
false;
216 const auto fn = fi.fileName();
217 const auto prefIdx = fn.indexOf(_pref);
219 fn.mid(prefIdx + _pref.length(),
220 fn.length() - prefIdx - _suff.length() - _pref.length());
223 d->locales.push_back(l);
224 qCDebug(C_LANGSELECT)
225 <<
"Added locale" << locPart <<
"to the list of supported locales.";
228 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locPart
229 <<
"to the list of supported locales.";
233 d->locales.squeeze();
236 qCWarning(C_LANGSELECT)
237 <<
"Can not find translation files for" << filter <<
"in" << path;
240 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
243 qCWarning(C_LANGSELECT) <<
"Can not set locales from dir with empty path or name.";
252 const QDir dir(path);
253 if (Q_LIKELY(dir.
exists())) {
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;
263 d->locales.push_back(l);
264 qCDebug(C_LANGSELECT)
265 <<
"Added locale" << subDir <<
"to the list of supported locales.";
268 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << subDir
269 <<
"to the list of supported locales.";
276 d->locales.squeeze();
280 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
283 qCWarning(C_LANGSELECT) <<
"Can not set locales from dirs with empty path or names.";
308 d->cookieName = name;
314 d->subDomainMap.clear();
316 d->locales.reserve(map.
size());
319 d->subDomainMap.insert(key, value);
320 d->locales.append(value);
322 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << value <<
"for subdomain"
323 << key <<
"to the subdomain map.";
326 d->locales.squeeze();
332 d->domainMap.clear();
334 d->locales.reserve(map.
size());
336 if (Q_LIKELY(value.language() !=
QLocale::C)) {
337 d->domainMap.insert(key, value);
338 d->locales.append(value);
340 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << value <<
"for domain" << key
341 <<
"to the domain map.";
344 d->locales.squeeze();
350 d->fallbackLocale = fallback;
356 d->detectFromHeader = enabled;
362 if (Q_LIKELY(!key.
isEmpty())) {
363 d->langStashKey = key;
365 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. "
366 "Using current key name"
374 if (Q_LIKELY(!key.
isEmpty())) {
375 d->dirStashKey = key;
377 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash "
378 "key. Using current key name"
386 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
390 return lsp->supportedLocales();
396 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
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)) {
406 d->setToQuery(c, _key);
410 d->setContentLanguage(c);
417 bool foundInSession =
false;
420 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
421 return foundInSession;
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)) {
431 d->setToSession(c, _key);
433 d->setContentLanguage(c);
435 return foundInSession;
440 bool foundInCookie =
false;
443 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
444 return foundInCookie;
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)) {
454 d->setToCookie(c, _name);
456 d->setContentLanguage(c);
458 return foundInCookie;
463 bool foundInSubDomain =
false;
466 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
467 return foundInSubDomain;
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)) {
479 d->setContentLanguage(c);
481 return foundInSubDomain;
486 bool foundInDomain =
false;
489 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
490 return foundInDomain;
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)) {
502 d->setContentLanguage(c);
504 return foundInDomain;
510 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
514 const auto d = lsp->d_ptr.get();
517 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
519 d->setContentLanguage(c);
522 if (!d->getFromHeader(c)) {
525 auto uri = c->
req()->uri();
527 const auto localeIdx = pathParts.
indexOf(locale);
529 uri.setPath(pathParts.join(u
'/'));
530 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
531 c->
res()->
redirect(uri, Response::TemporaryRedirect);
539 bool redirect =
false;
544 if (getFromSession(c, sessionKey)) {
548 if (getFromCookie(c, cookieName)) {
552 if (getFromQuery(c, queryKey)) {
556 if (getFromSubdomain(c, subDomainMap)) {
560 if (getFromDomain(c, domainMap)) {
575 if (foundIn != _source) {
577 setToSession(c, sessionKey);
579 setToCookie(c, cookieName);
581 setToQuery(c, queryKey);
590 setContentLanguage(c);
596bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const
599 if (l.language() !=
QLocale::C && locales.contains(l)) {
600 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
604 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
612 if (l.language() !=
QLocale::C && locales.contains(l)) {
613 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
617 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
622bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const
626 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
630 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
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"
648 if (domainParts.size() > 2) {
649 const QLocale l(domainParts.at(0));
651 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain"
657 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
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"
674 if (domainParts.size() > 1) {
675 const QLocale l(domainParts.at(domainParts.size() - 1));
677 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
682 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
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;
699 langPart = al.
left(idx);
701 priority = ref.
mid(ref.indexOf(u
'=') + 1).
toFloat(&ok);
707 const auto search = langMap.find(priority);
708 if (search == langMap.cend()) {
709 langMap.insert({priority, locale});
714 if (!langMap.empty()) {
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";
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()) {
736 qCDebug(C_LANGSELECT)
737 <<
"Selected locale" << c->locale() <<
"from" << name <<
"header";
753void LangSelectPrivate::setToQuery(
const Context *c,
const QString &key)
const
755 auto uri = c->
req()->uri();
757 if (query.hasQueryItem(key)) {
758 query.removeQueryItem(key);
762 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in URL query by redirecting to"
764 c->
res()->
redirect(uri, Response::TemporaryRedirect);
767void LangSelectPrivate::setToCookie(
const Context *c,
const QByteArray &name)
const
769 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in cookie with name" << name;
771 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
772 if (cookieExpiration.count() == 0) {
777 cookie.setDomain(cookieDomain);
778 cookie.setSecure(cookieSecure);
779 cookie.setSameSitePolicy(cookieSameSite);
783void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
785 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
789void LangSelectPrivate::setFallback(
Context *c)
const
791 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
795void LangSelectPrivate::setContentLanguage(
Context *c)
const
797 if (addContentLanguageHeader) {
801 {{langStashKey, c->
locale().bcp47Name()},
805void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
811 if (!c->
stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
815 detectLocale(c, source, skipMethod);
817 c->
setStash(LangSelectPrivate::stashKeySelectionTried,
true);
820void LangSelectPrivate::_q_postFork(
Application *app)
825#include "moc_langselect.cpp"
The Cutelyst application.
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
void postForked(Cutelyst::Application *app)
void stash(const QVariantHash &unite)
void detach(Action *action=nullptr)
QLocale locale() const noexcept
Response * res() const noexcept
void setStash(const QString &key, const QVariant &value)
void setLocale(const QLocale &locale)
QVariantMap config(const QString &entity) const
Detect and select locale based on different input parameters.
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
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.
Headers headers() const noexcept
QString queryParam(const QString &key, const QString &defaultValue={}) const
QByteArray cookie(QAnyStringView name) const
void redirect(const QUrl &url, quint16 status=Found)
void setHeader(const QByteArray &key, const QByteArray &value)
void setCookie(const QNetworkCookie &cookie)
Plugin providing methods for session management.
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
static void setValue(Context *c, const QString &key, const QVariant &value)
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
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
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
QString host(QUrl::ComponentFormattingOptions options) const const
QString path(QUrl::ComponentFormattingOptions options) const const
QLocale toLocale() const const