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)
31 const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_s};
33 LangSelect::LangSelect(
Application *parent, Cutelyst::LangSelect::Source source)
35 , d_ptr(new LangSelectPrivate)
44 , d_ptr(new LangSelectPrivate)
47 d->source = AcceptHeader;
48 d->autoDetect =
false;
51 LangSelect::~LangSelect() =
default;
57 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_LangSelect_Plugin"_s);
59 bool cookieExpirationOk =
false;
61 config.value(u
"cookie_expiration"_s, static_cast<qint64>(d->cookieExpiration.count()))
63 d->cookieExpiration = std::chrono::duration_cast<std::chrono::seconds>(
65 if (!cookieExpirationOk) {
66 qCWarning(C_LANGSELECT).nospace() <<
"Invalid value set for cookie_expiration. " 67 "Using default value " 68 #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) 69 << LangSelectPrivate::cookieDefaultExpiration;
73 d->cookieExpiration = LangSelectPrivate::cookieDefaultExpiration;
76 d->cookieDomain = config.value(u
"cookie_domain"_s).toString();
78 const QString _sameSite = config.value(u
"cookie_same_site"_s, u
"lax"_s).toString();
80 d->cookieSameSite = QNetworkCookie::SameSite::Default;
82 d->cookieSameSite = QNetworkCookie::SameSite::None;
84 d->cookieSameSite = QNetworkCookie::SameSite::Strict;
86 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
88 qCWarning(C_LANGSELECT).nospace() <<
"Invalid value set for cookie_same_site. " 89 "Using default value " 90 << QNetworkCookie::SameSite::Lax;
91 d->cookieSameSite = QNetworkCookie::SameSite::Lax;
94 d->cookieSecure = config.value(u
"cookie_secure"_s).toBool();
96 if ((d->cookieSameSite == QNetworkCookie::SameSite::None) && !d->cookieSecure) {
97 qCWarning(C_LANGSELECT) <<
"cookie_same_site has been set to None but cookie_secure is " 98 "not set to true. Implicitely setting cookie_secure to true. " 99 "Please check your configuration.";
100 d->cookieSecure =
true;
103 if (d->fallbackLocale.language() ==
QLocale::C) {
104 qCCritical(C_LANGSELECT) <<
"We need a valid fallback locale.";
108 if (d->source < Fallback) {
109 if (d->source == URLQuery && d->queryKey.isEmpty()) {
110 qCCritical(C_LANGSELECT) <<
"Can not use url query as source with empty key name.";
112 }
else if (d->source ==
Session && d->sessionKey.isEmpty()) {
113 qCCritical(C_LANGSELECT) <<
"Can not use session as source with empty key name.";
115 }
else if (d->source == Cookie && d->cookieName.isEmpty()) {
116 qCCritical(C_LANGSELECT) <<
"Can not use cookie as source with empty cookie name.";
120 qCCritical(C_LANGSELECT) <<
"Invalid source.";
124 d->beforePrepareAction(c, skipMethod);
127 if (!d->locales.contains(d->fallbackLocale)) {
128 d->locales.append(d->fallbackLocale);
132 qCDebug(C_LANGSELECT) <<
"Initialized LangSelect plugin with the following settings:";
133 qCDebug(C_LANGSELECT) <<
"Supported locales:" << d->locales;
134 qCDebug(C_LANGSELECT) <<
"Fallback locale:" << d->fallbackLocale;
135 qCDebug(C_LANGSELECT) <<
"Auto detection source:" << d->source;
136 qCDebug(C_LANGSELECT) <<
"Detect from header:" << d->detectFromHeader;
145 d->locales.reserve(locales.
size());
146 for (
const QLocale &l : locales) {
148 d->locales.push_back(l);
150 qCWarning(C_LANGSELECT)
151 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
156 void LangSelect::setSupportedLocales(
const QStringList &locales)
160 d->locales.reserve(locales.
size());
161 for (
const QString &l : locales) {
163 if (Q_LIKELY(locale.language() !=
QLocale::C)) {
164 d->locales.push_back(locale);
166 qCWarning(C_LANGSELECT)
167 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
172 void LangSelect::addSupportedLocale(
const QLocale &locale)
176 d->locales.push_back(locale);
178 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
179 <<
"to the list of supported locales.";
183 void LangSelect::addSupportedLocale(
const QString &locale)
188 d->locales.push_back(l);
190 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
191 <<
"to the list of supported locales.";
195 void LangSelect::setLocalesFromDir(
const QString &path,
203 const QDir dir(path);
204 if (Q_LIKELY(dir.exists())) {
205 const auto _pref = prefix.
isEmpty() ? u
"."_s : prefix;
206 const auto _suff = suffix.
isEmpty() ? u
".qm"_s : suffix;
207 const QString filter = name + _pref + u
'*' + _suff;
208 const auto files = dir.entryInfoList({name},
QDir::Files);
209 if (Q_LIKELY(!files.empty())) {
210 d->locales.
reserve(files.size());
211 bool shrinkToFit =
false;
213 const auto fn = fi.fileName();
214 const auto prefIdx = fn.indexOf(_pref);
216 fn.mid(prefIdx + _pref.length(),
217 fn.length() - prefIdx - _suff.length() - _pref.length());
220 d->locales.push_back(l);
221 qCDebug(C_LANGSELECT)
222 <<
"Added locale" << locPart <<
"to the list of supported locales.";
225 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locPart
226 <<
"to the list of supported locales.";
230 d->locales.squeeze();
233 qCWarning(C_LANGSELECT)
234 <<
"Can not find translation files for" << filter <<
"in" << path;
237 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
240 qCWarning(C_LANGSELECT) <<
"Can not set locales from dir with empty path or name.";
244 void LangSelect::setLocalesFromDirs(
const QString &path,
const QString &name)
249 const QDir dir(path);
250 if (Q_LIKELY(dir.exists())) {
252 if (Q_LIKELY(!dirs.empty())) {
253 d->locales.reserve(dirs.size());
254 bool shrinkToFit =
false;
255 for (
const QString &subDir : dirs) {
256 const QString relFn = subDir + u
'/' + name;
257 if (dir.exists(relFn)) {
260 d->locales.push_back(l);
261 qCDebug(C_LANGSELECT)
262 <<
"Added locale" << subDir <<
"to the list of supported locales.";
265 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << subDir
266 <<
"to the list of supported locales.";
273 d->locales.squeeze();
277 qCWarning(C_LANGSELECT) <<
"Can not set locales from not existing directory" << path;
280 qCWarning(C_LANGSELECT) <<
"Can not set locales from dirs with empty path or names.";
290 void LangSelect::setQueryKey(
const QString &key)
296 void LangSelect::setSessionKey(
const QString &key)
302 void LangSelect::setCookieName(
const QByteArray &name)
305 d->cookieName = name;
311 d->subDomainMap.clear();
313 d->locales.reserve(map.
size());
317 d->subDomainMap.insert(i.key(), i.value());
318 d->locales.append(i.value());
320 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.
value() <<
"for subdomain" 321 << i.key() <<
"to the subdomain map.";
325 d->locales.squeeze();
331 d->domainMap.clear();
333 d->locales.reserve(map.
size());
336 if (Q_LIKELY(i.value().language() !=
QLocale::C)) {
337 d->domainMap.insert(i.key(), i.value());
338 d->locales.append(i.value());
340 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << i.
value() <<
"for domain" 341 << i.key() <<
"to the domain map.";
345 d->locales.squeeze();
348 void LangSelect::setFallbackLocale(
const QLocale &fallback)
351 d->fallbackLocale = fallback;
354 void LangSelect::setDetectFromHeader(
bool enabled)
357 d->detectFromHeader = enabled;
360 void LangSelect::setLanguageCodeStashKey(
const QString &key)
363 if (Q_LIKELY(!key.
isEmpty())) {
364 d->langStashKey = key;
366 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language code stash key. " 367 "Using current key name" 372 void LangSelect::setLanguageDirStashKey(
const QString &key)
375 if (Q_LIKELY(!key.
isEmpty())) {
376 d->dirStashKey = key;
378 qCWarning(C_LANGSELECT) <<
"Can not set an empty key name for the language direction stash " 379 "key. Using current key name" 387 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
391 return lsp->supportedLocales();
397 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
401 const auto d = lsp->d_ptr.get();
402 const auto _key = !key.
isEmpty() ? key : d->queryKey;
403 if (!d->getFromQuery(c, _key)) {
404 if (!d->getFromHeader(c)) {
407 d->setToQuery(c, _key);
411 d->setContentLanguage(c);
418 bool foundInSession =
false;
421 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
422 return foundInSession;
425 const auto d = lsp->d_ptr.get();
426 const auto _key = !key.
isEmpty() ? key : d->sessionKey;
427 foundInSession = d->getFromSession(c, _key);
428 if (!foundInSession) {
429 if (!d->getFromHeader(c)) {
432 d->setToSession(c, _key);
434 d->setContentLanguage(c);
436 return foundInSession;
441 bool foundInCookie =
false;
444 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
445 return foundInCookie;
448 const auto d = lsp->d_ptr.get();
449 const auto _name = !name.
isEmpty() ? name : d->cookieName;
450 foundInCookie = d->getFromCookie(c, _name);
451 if (!foundInCookie) {
452 if (!d->getFromHeader(c)) {
455 d->setToCookie(c, _name);
457 d->setContentLanguage(c);
459 return foundInCookie;
464 bool foundInSubDomain =
false;
467 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
468 return foundInSubDomain;
471 const auto d = lsp->d_ptr.get();
472 const auto _map = !subDomainMap.
empty() ? subDomainMap : d->subDomainMap;
473 foundInSubDomain = d->getFromSubdomain(c, _map);
474 if (!foundInSubDomain) {
475 if (!d->getFromHeader(c)) {
480 d->setContentLanguage(c);
482 return foundInSubDomain;
487 bool foundInDomain =
false;
490 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
491 return foundInDomain;
494 const auto d = lsp->d_ptr.get();
495 const auto _map = !domainMap.
empty() ? domainMap : d->domainMap;
496 foundInDomain = d->getFromDomain(c, _map);
497 if (!foundInDomain) {
498 if (!d->getFromHeader(c)) {
503 d->setContentLanguage(c);
505 return foundInDomain;
511 qCCritical(C_LANGSELECT) <<
"LangSelect plugin not registered";
515 const auto d = lsp->d_ptr.get();
517 if (l.language() !=
QLocale::C && d->locales.contains(l)) {
518 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in path";
520 d->setContentLanguage(c);
523 if (!d->getFromHeader(c)) {
526 auto uri = c->
req()->uri();
528 const auto localeIdx = pathParts.
indexOf(locale);
530 uri.setPath(pathParts.join(u
'/'));
531 qCDebug(C_LANGSELECT) <<
"Storing selected locale by redirecting to" << uri;
532 c->
res()->
redirect(uri, Response::TemporaryRedirect);
538 bool LangSelectPrivate::detectLocale(
Context *c, LangSelect::Source _source,
bool *skipMethod)
const 540 bool redirect =
false;
542 LangSelect::Source foundIn = LangSelect::Fallback;
544 if (_source == LangSelect::Session) {
545 if (getFromSession(c, sessionKey)) {
548 }
else if (_source == LangSelect::Cookie) {
549 if (getFromCookie(c, cookieName)) {
552 }
else if (_source == LangSelect::URLQuery) {
553 if (getFromQuery(c, queryKey)) {
556 }
else if (_source == LangSelect::SubDomain) {
557 if (getFromSubdomain(c, subDomainMap)) {
560 }
else if (_source == LangSelect::Domain) {
561 if (getFromDomain(c, domainMap)) {
568 if (foundIn == LangSelect::Fallback && getFromHeader(c)) {
569 foundIn = LangSelect::AcceptHeader;
572 if (foundIn == LangSelect::Fallback) {
576 if (foundIn != _source) {
577 if (_source == LangSelect::Session) {
578 setToSession(c, sessionKey);
579 }
else if (_source == LangSelect::Cookie) {
580 setToCookie(c, cookieName);
581 }
else if (_source == LangSelect::URLQuery) {
582 setToQuery(c, queryKey);
591 setContentLanguage(c);
597 bool LangSelectPrivate::getFromQuery(
Context *c,
const QString &key)
const 600 if (l.language() !=
QLocale::C && locales.contains(l)) {
601 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in url query key" << key;
605 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in url query key" << key;
610 bool LangSelectPrivate::getFromCookie(
Context *c,
const QByteArray &cookie)
const 613 if (l.language() !=
QLocale::C && locales.contains(l)) {
614 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in cookie name" << cookie;
618 qCDebug(C_LANGSELECT) <<
"Can no find supported locale in cookie value with name" << cookie;
623 bool LangSelectPrivate::getFromSession(
Context *c,
const QString &key)
const 627 qCDebug(C_LANGSELECT) <<
"Found valid locale" << l <<
"in session key" << key;
631 qCDebug(C_LANGSELECT) <<
"Can not find supported locale in session value with key" << key;
638 const auto domain = c->
req()->uri().
host();
641 if (domain.startsWith(i.key())) {
642 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.
value()
643 <<
"in subdomain map for domain" << domain;
651 if (domainParts.size() > 2) {
652 const QLocale l(domainParts.at(0));
654 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in subdomain of domain" 660 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for subdomain" << domain;
666 const auto domain = c->
req()->uri().
host();
669 if (domain.endsWith(i.key())) {
670 qCDebug(C_LANGSELECT) <<
"Found valid locale" << i.
value() <<
"in domain map for domain" 679 if (domainParts.size() > 1) {
680 const QLocale l(domainParts.at(domainParts.size() - 1));
682 qCDebug(C_LANGSELECT) <<
"Found supported locale" << l <<
"in domain" << domain;
687 qCDebug(C_LANGSELECT) <<
"Can not find supported locale for domain" << domain;
693 if (detectFromHeader) {
696 if (Q_LIKELY(!accpetedLangs.empty())) {
697 std::map<float, QLocale> langMap;
698 for (
const auto &ba : accpetedLangs) {
700 const auto idx = al.
indexOf(u
';');
701 float priority = 1.0f;
705 langPart = al.
left(idx);
707 priority = ref.
mid(ref.indexOf(u
'=') + 1).toFloat(&ok);
713 const auto search = langMap.find(priority);
714 if (search == langMap.cend()) {
715 langMap.insert({priority, locale});
719 if (!langMap.empty()) {
720 auto i = langMap.crbegin();
721 while (i != langMap.crend()) {
722 if (locales.contains(i->second)) {
724 qCDebug(C_LANGSELECT)
725 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
732 i = langMap.crbegin();
733 const auto constLocales = locales;
734 while (i != langMap.crend()) {
735 for (
const QLocale &l : constLocales) {
736 if (l.
language() == i->second.language()) {
738 qCDebug(C_LANGSELECT)
739 <<
"Selected locale" << c->
locale() <<
"from" << name <<
"header";
752 void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const 754 auto uri = c->
req()->uri();
756 if (query.hasQueryItem(key)) {
757 query.removeQueryItem(key);
761 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in URL query by redirecting to" 763 c->
res()->
redirect(uri, Response::TemporaryRedirect);
768 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in cookie with name" << name;
770 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
771 if (cookieExpiration.count() == 0) {
776 cookie.setDomain(cookieDomain);
777 cookie.setSecure(cookieSecure);
778 cookie.setSameSitePolicy(cookieSameSite);
782 void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const 784 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
788 void LangSelectPrivate::setFallback(
Context *c)
const 790 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
794 void LangSelectPrivate::setContentLanguage(
Context *c)
const 796 if (addContentLanguageHeader) {
804 void LangSelectPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
const 810 if (!c->
stash(LangSelectPrivate::stashKeySelectionTried).isNull()) {
814 detectLocale(c, source, skipMethod);
816 c->
setStash(LangSelectPrivate::stashKeySelectionTried,
true);
819 void LangSelectPrivate::_q_postFork(
Application *app)
824 #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
const_iterator constBegin() const const
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)
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
bool isEmpty() const const
const_iterator constEnd() const const
Qt::LayoutDirection textDirection() const const
QDateTime currentDateTime()
QString path(ComponentFormattingOptions options) const const
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.
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)
size_type size() const const
void setHeader(const QByteArray &key, const QByteArray &value)