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>
23 Q_LOGGING_CATEGORY(C_LANGSELECT,
"cutelyst.plugin.langselect", QtWarningMsg)
30 const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_qs};
32 LangSelect::LangSelect(
Application *parent, Cutelyst::LangSelect::Source source)
34 , d_ptr(new LangSelectPrivate)
43 , d_ptr(new LangSelectPrivate)
46 d->source = AcceptHeader;
47 d->autoDetect =
false;
50 LangSelect::~LangSelect() =
default;
56 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_LangSelect_Plugin"_qs);
58 bool cookieExpirationOk =
false;
60 config.value(u
"cookie_expiration"_qs,
static_cast<qint64
>(d->cookieExpiration.count()))
62 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
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;
72 d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
75 d->cookieDomain = config.value(u
"cookie_domain"_qs).toString();
77 const QString _sameSite = config.value(u
"cookie_same_site"_qs, u
"lax"_qs).toString();
79 d->cookieSameSite = QNetworkCookie::SameSite::Default;
81 d->cookieSameSite = QNetworkCookie::SameSite::None;
83 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
85 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
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;
93 d->cookieSecure = config.value(u
"cookie_secure"_qs).toBool();
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;
102 if (d->fallbackLocale.language() ==
QLocale::C) {
103 qCCritical(C_LANGSELECT) <<
"We need a valid fallback locale.";
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.";
111 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
112 qCCritical(C_LANGSELECT) <<
"Can not use session as source with empty key name.";
114 }
else if (d->source == Cookie && d->cookieName.isEmpty()) {
115 qCCritical(C_LANGSELECT) <<
"Can not use cookie as source with empty cookie name.";
119 qCCritical(C_LANGSELECT) <<
"Invalid source.";
123 d->beforePrepareAction(c, skipMethod);
126 if (!d->locales.contains(d->fallbackLocale)) {
127 d->locales.append(d->fallbackLocale);
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;
144 d->locales.reserve(locales.
size());
145 for (
const QLocale &l : locales) {
147 d->locales.push_back(l);
149 qCWarning(C_LANGSELECT)
150 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
155 void LangSelect::setSupportedLocales(
const QStringList &locales)
159 d->locales.reserve(locales.
size());
160 for (
const QString &l : locales) {
162 if (Q_LIKELY(locale.language() !=
QLocale::C)) {
163 d->locales.push_back(locale);
165 qCWarning(C_LANGSELECT)
166 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
171 void LangSelect::addSupportedLocale(
const QLocale &locale)
175 d->locales.push_back(locale);
177 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
178 <<
"to the list of supported locales.";
182 void LangSelect::addSupportedLocale(
const QString &locale)
187 d->locales.push_back(l);
189 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
190 <<
"to the list of supported locales.";
194 void LangSelect::setLocalesFromDir(
const QString &path,
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;
212 const auto fn = fi.fileName();
213 const auto prefIdx = fn.indexOf(_pref);
215 fn.mid(prefIdx + _pref.length(),
216 fn.length() - prefIdx - _suff.length() - _pref.length());
219 d->locales.push_back(l);
220 qCDebug(C_LANGSELECT)
221 <<
"Added locale" << locPart <<
"to the list of supported locales.";
224 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locPart
225 <<
"to the list of supported locales.";
229 d->locales.squeeze();
232 qCWarning(C_LANGSELECT)
233 <<
"Can not find translation files for" << filter <<
"in" << path;
236 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
239 qCWarning(C_LANGSELECT) <<
"Can not set locales from dir with empty path or name.";
243 void LangSelect::setLocalesFromDirs(
const QString &path,
const QString &name)
248 const QDir dir(path);
249 if (Q_LIKELY(dir.exists())) {
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)) {
259 d->locales.push_back(l);
260 qCDebug(C_LANGSELECT)
261 <<
"Added locale" << subDir <<
"to the list of supported locales.";
264 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << subDir
265 <<
"to the list of supported locales.";
272 d->locales.squeeze();
276 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
279 qCWarning(C_LANGSELECT) <<
"Can not set locales from dirs with empty path or names.";
289 void LangSelect::setQueryKey(
const QString &key)
295 void LangSelect::setSessionKey(
const QString &key)
301 void LangSelect::setCookieName(
const QByteArray &name)
304 d->cookieName = name;
310 d->subDomainMap.clear();
312 d->locales.reserve(map.
size());
316 d->subDomainMap.insert(i.key(), i.value());
317 d->locales.append(i.value());
319 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.
value() <<
"for subdomain"
320 << i.key() <<
"to the subdomain map.";
324 d->locales.squeeze();
330 d->domainMap.clear();
332 d->locales.reserve(map.
size());
335 if (Q_LIKELY(i.value().language() !=
QLocale::C)) {
336 d->domainMap.insert(i.key(), i.value());
337 d->locales.append(i.value());
339 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.
value() <<
"for domain"
340 << i.key() <<
"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();
640 if (domain.startsWith(i.key())) {
641 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.
value()
642 <<
"in subdomain map for domain" << domain;
650 if (domainParts.size() > 2) {
651 const QLocale l(domainParts.at(0));
653 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain"
659 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
665 const auto domain = c->
req()->uri().
host();
668 if (domain.endsWith(i.key())) {
669 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.
value() <<
"in domain map for domain"
678 if (domainParts.size() > 1) {
679 const QLocale l(domainParts.at(domainParts.size() - 1));
681 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
686 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
692 if (detectFromHeader) {
695 if (Q_LIKELY(!accpetedLangs.empty())) {
696 std::map<float, QLocale> langMap;
697 for (
const auto &ba : accpetedLangs) {
699 const auto idx = al.
indexOf(u
';');
700 float priority = 1.0f;
704 langPart = al.
left(idx);
706 priority = ref.
mid(ref.indexOf(u
'=') + 1).
toFloat(&ok);
712 const auto search = langMap.find(priority);
713 if (search == langMap.cend()) {
714 langMap.insert({priority, locale});
718 if (!langMap.empty()) {
719 auto i = langMap.crbegin();
720 while (i != langMap.crend()) {
721 if (locales.contains(i->second)) {
723 qCDebug(C_LANGSELECT)
724 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
732 const auto constLocales = locales;
733 while (i != langMap.crend()) {
734 for (
const QLocale &l : constLocales) {
735 if (l.
language() == i->second.language()) {
737 qCDebug(C_LANGSELECT)
738 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
751 void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
753 auto uri = c->
req()->uri();
755 if (query.hasQueryItem(key)) {
756 query.removeQueryItem(key);
760 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in URL query by redirecting to"
762 c->
res()->
redirect(uri, Response::TemporaryRedirect);
767 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in cookie with name" << name;
769 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
770 if (cookieExpiration.count() == 0) {
775 cookie.setDomain(cookieDomain);
776 cookie.setSecure(cookieSecure);
777 cookie.setSameSitePolicy(cookieSameSite);
781 void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
783 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
787 void LangSelectPrivate::setFallback(
Context *c)
const
789 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
793 void LangSelectPrivate::setContentLanguage(
Context *c)
const
795 if (addContentLanguageHeader) {
799 {{langStashKey, c->
locale().bcp47Name()},
803 void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const
809 if (!c->
stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
813 detectLocale(c, source, skipMethod);
815 c->
setStash(LangSelectPrivate::stashKeySelectionTried,
true);
818 void LangSelectPrivate::_q_postFork(
Application *app)
823 #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.
Base class for Cutelyst Plugins.
QByteArray cookie(QByteArrayView name) const
QString queryParam(const QString &key, const QString &defaultValue={}) const
QByteArray header(QByteArrayView key) const noexcept
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
QList< QByteArray > split(char sep) const const
QDateTime currentDateTime()
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
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
void reserve(qsizetype size)
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