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> 24 Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
29 const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_s};
36 LangSelect::LangSelect(
Application *parent, Cutelyst::LangSelect::Source source)
38 , d_ptr(new LangSelectPrivate)
47 , d_ptr(new LangSelectPrivate)
50 d->source = AcceptHeader;
51 d->autoDetect =
false;
54 LangSelect::~LangSelect() =
default;
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.";
111 if (d->source < Fallback) {
112 if (d->source == URLQuery && d->queryKey.isEmpty()) {
113 qCCritical(C_LANGSELECT) <<
"Can not use url query as source with empty key name.";
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.";
127 d->beforePrepareAction(c, skipMethod);
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.";
159 void LangSelect::setSupportedLocales(
const QStringList &locales)
163 d->locales.reserve(locales.
size());
164 for (
const QString &l : locales) {
166 if (Q_LIKELY(locale.language() !=
QLocale::C)) {
167 d->locales.push_back(locale);
169 qCWarning(C_LANGSELECT)
170 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
175 void LangSelect::addSupportedLocale(
const QLocale &locale)
179 d->locales.push_back(locale);
181 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
182 <<
"to the list of supported locales.";
186 void LangSelect::addSupportedLocale(
const QString &locale)
191 d->locales.push_back(l);
193 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
194 <<
"to the list of supported locales.";
198 void LangSelect::setLocalesFromDir(
const QString &path,
206 const QDir dir(path);
207 if (Q_LIKELY(dir.exists())) {
208 const auto _pref = prefix.
isEmpty() ? u
"."_s : prefix;
209 const auto _suff = suffix.
isEmpty() ? u
".qm"_s : suffix;
210 const QString filter = name + _pref + u
'*' + _suff;
211 const auto files = dir.entryInfoList({name},
QDir::Files);
212 if (Q_LIKELY(!files.empty())) {
213 d->locales.
reserve(files.size());
214 bool shrinkToFit =
false;
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.";
247 void LangSelect::setLocalesFromDirs(
const QString &path,
const QString &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;
260 if (dir.exists(relFn)) {
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.";
293 void LangSelect::setQueryKey(
const QString &key)
299 void LangSelect::setSessionKey(
const QString &key)
305 void LangSelect::setCookieName(
const QByteArray &name)
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();
347 void LangSelect::setFallbackLocale(
const QLocale &fallback)
350 d->fallbackLocale = fallback;
353 void LangSelect::setDetectFromHeader(
bool enabled)
356 d->detectFromHeader = enabled;
359 void LangSelect::setLanguageCodeStashKey(
const QString &key)
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" 371 void LangSelect::setLanguageDirStashKey(
const QString &key)
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();
516 if (l.language() !=
QLocale::C && d->locales.contains(l)) {
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);
537 bool LangSelectPrivate::detectLocale(
Context *c, LangSelect::Source _source,
bool *skipMethod)
const 539 bool redirect =
false;
541 LangSelect::Source foundIn = LangSelect::Fallback;
543 if (_source == LangSelect::Session) {
544 if (getFromSession(c, sessionKey)) {
547 }
else if (_source == LangSelect::Cookie) {
548 if (getFromCookie(c, cookieName)) {
551 }
else if (_source == LangSelect::URLQuery) {
552 if (getFromQuery(c, queryKey)) {
555 }
else if (_source == LangSelect::SubDomain) {
556 if (getFromSubdomain(c, subDomainMap)) {
559 }
else if (_source == LangSelect::Domain) {
560 if (getFromDomain(c, domainMap)) {
567 if (foundIn == LangSelect::Fallback && getFromHeader(c)) {
568 foundIn = LangSelect::AcceptHeader;
571 if (foundIn == LangSelect::Fallback) {
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);
590 setContentLanguage(c);
596 bool 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;
609 bool LangSelectPrivate::getFromCookie(
Context *c,
const QByteArray &cookie)
const 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;
622 bool 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();
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();
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)) {
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";
753 void 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);
767 void 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);
783 void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const 785 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
789 void LangSelectPrivate::setFallback(
Context *c)
const 791 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
795 void LangSelectPrivate::setContentLanguage(
Context *c)
const 797 if (addContentLanguageHeader) {
805 void 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);
820 void LangSelectPrivate::_q_postFork(
Application *app)
825 #include "moc_langselect.cpp" void setCookie(const QNetworkCookie &cookie)
void postForked(Cutelyst::Application *app)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
Response * res() const noexcept
QStringView mid(qsizetype start, qsizetype length) const const
QString host(ComponentFormattingOptions options) const const
bool isEmpty() const const
void setStash(const QString &key, const QVariant &value)
void detach(Action *action=nullptr)
QString queryParam(const QString &key, const QString &defaultValue={}) const
qsizetype size() const const
void redirect(const QUrl &url, quint16 status=Found)
void stash(const QVariantHash &unite)
Headers headers() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
bool isEmpty() const const
Qt::LayoutDirection textDirection() const const
QDateTime currentDateTime()
QString path(ComponentFormattingOptions options) const const
Language language() const const
static void setValue(Context *c, const QString &key, const QVariant &value)
The Cutelyst namespace holds all public Cutelyst API.
QLocale locale() const noexcept
qsizetype indexOf(const QRegularExpression &re, qsizetype from) const const
QString toLower() const const
void setLocale(const QLocale &locale)
QString fromLatin1(QByteArrayView str)
Detect and select locale based on different input parameters.
QByteArray toLatin1() const const
bool setup(Application *app) override
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray cookie(QAnyStringView name) const
CUTELYST_EXPORT std::chrono::microseconds durationFromString(QStringView str, bool *ok=nullptr)
QString bcp47Name() const const
static QVariant value(Context *c, const QString &key, const QVariant &defaultValue=QVariant())
void reserve(qsizetype size)
QString left(qsizetype n) const const
QLocale toLocale() const const
Base class for Cutelyst Plugins.
The Cutelyst application.
Plugin providing methods for session management.
Engine * engine() const noexcept
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
size_type size() const const
void setHeader(const QByteArray &key, const QByteArray &value)