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";
731 i = langMap.crbegin();
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) {
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" void setCookie(const QNetworkCookie &cookie)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QByteArray header(QByteArrayView key) const noexcept
void postForked(Cutelyst::Application *app)
QList< QByteArray > split(char sep) const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
Response * res() const noexcept
QMap::const_iterator constBegin() const const
QStringView mid(qsizetype start, qsizetype length) const const
QString host(QUrl::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)
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
bool isEmpty() const const
QMap::const_iterator constEnd() const const
Qt::LayoutDirection textDirection() const const
QString path(QUrl::ComponentFormattingOptions options) const const
QLocale::Language language() const const
const T & value() 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.
QDateTime currentDateTime()
QByteArray toLatin1() const const
bool setup(Application *app) override
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const 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.
QByteArray cookie(QByteArrayView name) const
Plugin providing methods for session management.
Engine * engine() const noexcept
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QMap::size_type size() const const
void setHeader(const QByteArray &key, const QByteArray &value)