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)
31const QString LangSelectPrivate::stashKeySelectionTried{u
"_c_langselect_tried"_s};
35 , d_ptr(new LangSelectPrivate)
44 , d_ptr(new LangSelectPrivate)
48 d->autoDetect =
false;
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.";
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.";
160 d->locales.reserve(locales.
size());
161 for (
const QString &l : locales) {
164 d->locales.push_back(locale);
166 qCWarning(C_LANGSELECT)
167 <<
"Can not add invalid locale" << l <<
"to the list of supported locales.";
176 d->locales.push_back(locale);
178 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
179 <<
"to the list of supported locales.";
188 d->locales.push_back(l);
190 qCWarning(C_LANGSELECT) <<
"Can not add invalid locale" << locale
191 <<
"to the list of supported locales.";
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;
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.";
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;
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.";
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();
351 d->fallbackLocale = fallback;
357 d->detectFromHeader = enabled;
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"
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();
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);
540 bool redirect =
false;
545 if (getFromSession(c, sessionKey)) {
549 if (getFromCookie(c, cookieName)) {
553 if (getFromQuery(c, queryKey)) {
557 if (getFromSubdomain(c, subDomainMap)) {
561 if (getFromDomain(c, domainMap)) {
576 if (foundIn != _source) {
578 setToSession(c, sessionKey);
580 setToCookie(c, cookieName);
582 setToQuery(c, queryKey);
591 setContentLanguage(c);
597bool 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;
610bool 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;
623bool 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;
636bool LangSelectPrivate::getFromSubdomain(
Context *c,
const QMap<QString, QLocale> &map)
const
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;
664bool LangSelectPrivate::getFromDomain(
Context *c,
const QMap<QString, QLocale> &map)
const
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;
691bool LangSelectPrivate::getFromHeader(
Context *c,
const QByteArray &name)
const
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);
706 const auto ref = QStringView(al).
mid(idx + 1);
707 priority = ref.
mid(ref.indexOf(u
'=') + 1).
toFloat(&ok);
711 QLocale locale(langPart);
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";
752void LangSelectPrivate::setToQuery(
Context *c,
const QString &key)
const
754 auto uri = c->
req()->uri();
755 QUrlQuery query(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);
766void LangSelectPrivate::setToCookie(
Context *c,
const QByteArray &name)
const
768 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in cookie with name" << name;
770 cookie.setSameSitePolicy(QNetworkCookie::SameSite::Lax);
771 if (cookieExpiration.count() == 0) {
772 cookie.setExpirationDate(QDateTime());
776 cookie.setDomain(cookieDomain);
777 cookie.setSecure(cookieSecure);
778 cookie.setSameSitePolicy(cookieSameSite);
782void LangSelectPrivate::setToSession(
Context *c,
const QString &key)
const
784 qCDebug(C_LANGSELECT) <<
"Storing selected" << c->
locale() <<
"in session key" << key;
788void LangSelectPrivate::setFallback(
Context *c)
const
790 qCDebug(C_LANGSELECT) <<
"Can not find fitting locale, using fallback locale" << fallbackLocale;
794void LangSelectPrivate::setContentLanguage(
Context *c)
const
796 if (addContentLanguageHeader) {
804void 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);
819void LangSelectPrivate::_q_postFork(
Application *app)
824#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 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"))
static bool fromUrlQuery(Context *c, const QString &key=QString())
static bool fromSession(Context *c, const QString &key=QString())
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
void setSupportedLocales(const QVector< QLocale > &locales)
LangSelect(Application *parent, Source source)
Plugin(Application *parent)
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)
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.
bool isEmpty() const const
QList< QByteArray > split(char sep) 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
qsizetype size() const const
QString bcp47Name(QLocale::TagSeparator separator) const const
QLocale::Language language() const const
QMap< Key, T >::const_iterator constBegin() const const
QMap< Key, T >::const_iterator constEnd() const const
QMap< Key, T >::size_type size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
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) &&
QString mid(qsizetype position, qsizetype n) &&
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
float toFloat(bool *ok) const const
QByteArray toLatin1() const const
QString toLower() const const
qsizetype indexOf(QLatin1StringView str, qsizetype from, Qt::CaseSensitivity cs) const const
QString host(QUrl::ComponentFormattingOptions options) const const
QString path(QUrl::ComponentFormattingOptions options) const const
QLocale toLocale() const const