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>
19#include <QLoggingCategory>
23Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
28const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_s};
37 , d_ptr(new LangSelectPrivate)
46 , d_ptr(new LangSelectPrivate)
50 d->autoDetect =
false;
59 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_LangSelect_Plugin"_s);
61 bool cookieExpirationOk =
false;
63 config.value(u
"cookie_expiration"_s,
static_cast<qint64
>(d->cookieExpiration.count()))
65 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
67 if (!cookieExpirationOk) {
68 qCWarning(C_LANGSELECT).nospace() <<
"Invalid value set for cookie_expiration. "
69 "Using default value "
70#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
71 << LangSelectPrivate::cookieDefaultExpiration;
75 d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
78 d->cookieDomain = config.value(u
"cookie_domain"_s).toString();
80 const QString _sameSite = config.value(u
"cookie_same_site"_s, u
"lax"_s).toString();
82 d->cookieSameSite = QNetworkCookie::SameSite::Default;
84 d->cookieSameSite = QNetworkCookie::SameSite::None;
86 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
88 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
90 qCWarning(C_LANGSELECT).nospace() <<
"Invalid value set for cookie_same_site. "
91 "Using default value "
92 << QNetworkCookie::SameSite::Lax;
93 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
96 d->cookieSecure = config.value(u
"cookie_secure"_s).toBool();
98 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
99 qCWarning(C_LANGSELECT) <<
"cookie_same_site has been set to None but cookie_secure is "
100 "not set to true. Implicitely setting cookie_secure to true. "
101 "Please check your configuration.";
102 d->cookieSecure =
true;
105 if (d->fallbackLocale.language() ==
QLocale::C) {
106 qCCritical(C_LANGSELECT) <<
"We need a valid fallback locale.";
111 if (d->source ==
URLQuery && d->queryKey.isEmpty()) {
112 qCCritical(C_LANGSELECT) <<
"Can not use url query as source with empty key name.";
114 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
115 qCCritical(C_LANGSELECT) <<
"Can not use session as source with empty key name.";
117 }
else if (d->source ==
Cookie && d->cookieName.isEmpty()) {
118 qCCritical(C_LANGSELECT) <<
"Can not use cookie as source with empty cookie name.";
122 qCCritical(C_LANGSELECT) <<
"Invalid source.";
129 if (!d->locales.contains(d->fallbackLocale)) {
130 d->locales.append(d->fallbackLocale);
134 qCDebug(C_LANGSELECT) <<
"Initialized LangSelect plugin with the following settings:";
135 qCDebug(C_LANGSELECT) <<
"Supported locales:" << d->locales;
136 qCDebug(C_LANGSELECT) <<
"Fallback locale:" << d->fallbackLocale;
137 qCDebug(C_LANGSELECT) <<
"Auto detection source:" << d->source;
138 qCDebug(C_LANGSELECT) <<
"Detect from header:" << d->detectFromHeader;
147 d->locales.reserve(locales.
size());
148 for (
const QLocale &l : locales) {
150 d->locales.push_back(l);
152 qCWarning(C_LANGSELECT)
153 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
162 d->locales.reserve(locales.
size());
163 for (
const QString &l : locales) {
166 d->locales.push_back(locale);
168 qCWarning(C_LANGSELECT)
169 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
178 d->locales.push_back(locale);
180 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
181 <<
"to the list of supported locales.";
190 d->locales.push_back(l);
192 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
193 <<
"to the list of supported locales.";
205 const QDir dir(path);
206 if (Q_LIKELY(dir.
exists())) {
207 const auto _pref = prefix.
isEmpty() ? u
"."_s : prefix;
208 const auto _suff = suffix.
isEmpty() ? u
".qm"_s : suffix;
209 const QString filter = name + _pref + u
'*' + _suff;
211 if (Q_LIKELY(!files.empty())) {
212 d->locales.reserve(files.size());
213 bool shrinkToFit =
false;
215 const auto fn = fi.fileName();
216 const auto prefIdx = fn.indexOf(_pref);
218 fn.mid(prefIdx + _pref.length(),
219 fn.length() - prefIdx - _suff.length() - _pref.length());
222 d->locales.push_back(l);
223 qCDebug(C_LANGSELECT)
224 <<
"Added locale" << locPart <<
"to the list of supported locales.";
227 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locPart
228 <<
"to the list of supported locales.";
232 d->locales.squeeze();
235 qCWarning(C_LANGSELECT)
236 <<
"Can not find translation files for" << filter <<
"in" << path;
239 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
242 qCWarning(C_LANGSELECT) <<
"Can not set locales from dir with empty path or name.";
251 const QDir dir(path);
252 if (Q_LIKELY(dir.
exists())) {
254 if (Q_LIKELY(!dirs.empty())) {
255 d->locales.
reserve(dirs.size());
256 bool shrinkToFit =
false;
257 for (
const QString &subDir : dirs) {
258 const QString relFn = subDir + u
'/' + name;
262 d->locales.push_back(l);
263 qCDebug(C_LANGSELECT)
264 <<
"Added locale" << subDir <<
"to the list of supported locales.";
267 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << subDir
268 <<
"to the list of supported locales.";
275 d->locales.squeeze();
279 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
282 qCWarning(C_LANGSELECT) <<
"Can not set locales from dirs with empty path or names.";
307 d->cookieName = name;
313 d->subDomainMap.clear();
315 d->locales.reserve(map.
size());
318 d->subDomainMap.insert(key, value);
319 d->locales.append(value);
321 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << value <<
"for subdomain"
322 << key <<
"to the subdomain map.";
325 d->locales.squeeze();
331 d->domainMap.clear();
333 d->locales.reserve(map.
size());
335 if (Q_LIKELY(value.language() !=
QLocale::C)) {
336 d->domainMap.insert(key, value);
337 d->locales.append(value);
339 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << value <<
"for domain" << key
340 <<
"to the domain map.";
343 d->locales.squeeze();
349 d->fallbackLocale = fallback;
355 d->detectFromHeader = enabled;
361 if (Q_LIKELY(!key.
isEmpty())) {
362 d->langStashKey = key;
364 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. "
365 "Using current key name"
373 if (Q_LIKELY(!key.
isEmpty())) {
374 d->dirStashKey = key;
376 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash "
377 "key. Using current key name"
385 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
389 return lsp->supportedLocales();
395 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
399 const auto d = lsp->d_ptr.get();
400 const auto _key = !key.
isEmpty() ? key : d->queryKey;
401 if (!d->getFromQuery(c, _key)) {
402 if (!d->getFromHeader(c)) {
405 d->setToQuery(c, _key);
409 d->setContentLanguage(c);
416 bool foundInSession =
false;
419 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
420 return foundInSession;
423 const auto d = lsp->d_ptr.get();
424 const auto _key = !key.
isEmpty() ? key : d->sessionKey;
425 foundInSession = d->getFromSession(c, _key);
426 if (!foundInSession) {
427 if (!d->getFromHeader(c)) {
430 d->setToSession(c, _key);
432 d->setContentLanguage(c);
434 return foundInSession;
439 bool foundInCookie =
false;
442 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
443 return foundInCookie;
446 const auto d = lsp->d_ptr.get();
447 const auto _name = !name.
isEmpty() ? name : d->cookieName;
448 foundInCookie = d->getFromCookie(c, _name);
449 if (!foundInCookie) {
450 if (!d->getFromHeader(c)) {
453 d->setToCookie(c, _name);
455 d->setContentLanguage(c);
457 return foundInCookie;
462 bool foundInSubDomain =
false;
465 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
466 return foundInSubDomain;
469 const auto d = lsp->d_ptr.get();
470 const auto _map = !subDomainMap.
empty() ? subDomainMap : d->subDomainMap;
471 foundInSubDomain = d->getFromSubdomain(c, _map);
472 if (!foundInSubDomain) {
473 if (!d->getFromHeader(c)) {
478 d->setContentLanguage(c);
480 return foundInSubDomain;
485 bool foundInDomain =
false;
488 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
489 return foundInDomain;
492 const auto d = lsp->d_ptr.get();
493 const auto _map = !domainMap.
empty() ? domainMap : d->domainMap;
494 foundInDomain = d->getFromDomain(c, _map);
495 if (!foundInDomain) {
496 if (!d->getFromHeader(c)) {
501 d->setContentLanguage(c);
503 return foundInDomain;
509 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
513 const auto d = lsp->d_ptr.get();
516 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
518 d->setContentLanguage(c);
521 if (!d->getFromHeader(c)) {
524 auto uri = c->
req()->uri();
526 const auto localeIdx = pathParts.
indexOf(locale);
528 uri.setPath(pathParts.join(u
'/'));
529 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
530 c->
res()->
redirect(uri, Response::TemporaryRedirect);
538 bool redirect =
false;
543 if (getFromSession(c, sessionKey)) {
547 if (getFromCookie(c, cookieName)) {
551 if (getFromQuery(c, queryKey)) {
555 if (getFromSubdomain(c, subDomainMap)) {
559 if (getFromDomain(c, domainMap)) {
574 if (foundIn != _source) {
576 setToSession(c, sessionKey);
578 setToCookie(c, cookieName);
580 setToQuery(c, queryKey);
589 setContentLanguage(c);
595bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const
598 if (l.language() !=
QLocale::C && locales.contains(l)) {
599 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
603 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
611 if (l.language() !=
QLocale::C && locales.contains(l)) {
612 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
616 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
621bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const
625 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
629 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
636 const auto domain = c->
req()->uri().
host();
637 for (
const auto &[key, value] : map.asKeyValueRange()) {
638 if (domain.startsWith(key)) {
639 qCDebug(C_LANGSELECT) <<
"Found valid locale" << value <<
"in subdomain map for domain"
647 if (domainParts.size() > 2) {
648 const QLocale l(domainParts.at(0));
650 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain"
656 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
662 const auto domain = c->
req()->uri().
host();
663 for (
const auto &[key, value] : map.asKeyValueRange()) {
664 if (domain.endsWith(key)) {
665 qCDebug(C_LANGSELECT) <<
"Found valid locale" << value <<
"in domain map for domain"
673 if (domainParts.size() > 1) {
674 const QLocale l(domainParts.at(domainParts.size() - 1));
676 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
681 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
687 if (detectFromHeader) {
688 const auto accpetedLangs =
690 if (Q_LIKELY(!accpetedLangs.empty())) {
691 std::map<float, QLocale> langMap;
692 for (
const auto &al : accpetedLangs) {
693 const auto idx = al.
indexOf(u
';');
694 float priority = 1.0F;
698 langPart = al.
left(idx);
700 priority = ref.
mid(ref.indexOf(u
'=') + 1).
toFloat(&ok);
706 const auto search = langMap.find(priority);
707 if (search == langMap.cend()) {
708 langMap.insert({priority, locale});
712 if (!langMap.empty()) {
713 auto i = langMap.crbegin();
714 while (i != langMap.crend()) {
715 if (locales.contains(i->second)) {
717 qCDebug(C_LANGSELECT)
718 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
726 const auto constLocales = locales;
727 while (i != langMap.crend()) {
728 for (
const QLocale &l : constLocales) {
729 if (l.
language() == i->second.language()) {
731 qCDebug(C_LANGSELECT)
732 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
745void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
747 auto uri = c->
req()->uri();
749 if (query.hasQueryItem(key)) {
750 query.removeQueryItem(key);
754 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in URL query by redirecting to"
756 c->
res()->
redirect(uri, Response::TemporaryRedirect);
761 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in cookie with name" << name;
763 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
764 if (cookieExpiration.count() == 0) {
769 cookie.setDomain(cookieDomain);
770 cookie.setSecure(cookieSecure);
771 cookie.setSameSitePolicy(cookieSameSite);
775void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
777 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
781void LangSelectPrivate::setFallback(
Context *c)
const
783 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
787void LangSelectPrivate::setContentLanguage(
Context *c)
const
789 if (addContentLanguageHeader) {
793 {{langStashKey, c->
locale().bcp47Name()},
797void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
803 if (!c->
stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
807 detectLocale(c, source, skipMethod);
809 c->
setStash(LangSelectPrivate::stashKeySelectionTried,
true);
812void LangSelectPrivate::_q_postFork(
Application *app)
817#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.
QByteArray::const_reverse_iterator crbegin() const const
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