From d4a5c81034554b675d64129e0bc077c8ad422513 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Sat, 25 Aug 2018 22:56:33 +0200 Subject: [PATCH] KEYCLOAK-8146: Extract LocaleSelectorSPI to allow custom overrides of locale selection --- .../locale/LocaleSelectorProvider.java | 33 ++++ .../locale/LocaleSelectorProviderFactory.java | 22 +++ .../keycloak/locale/LocaleSelectorSPI.java | 45 +++++ .../services/org.keycloak.provider.Spi | 1 + .../locale/DefaultLocaleSelectorProvider.java | 160 ++++++++++++++++++ .../DefaultLocaleSelectorProviderFactory.java | 45 +++++ .../util => locale}/LocaleNegotiator.java | 13 +- .../org/keycloak/locale/LocaleSelection.java | 37 ++++ .../services/DefaultKeycloakContext.java | 4 +- .../services/error/KeycloakErrorHandler.java | 3 +- .../resources/account/AccountConsole.java | 3 +- .../keycloak/services/util/LocaleHelper.java | 146 ---------------- .../org/keycloak/services/util/P3PHelper.java | 3 +- ...cloak.locale.LocaleSelectorProviderFactory | 18 ++ .../keycloak/locale/LocaleNegotiatorTest.java | 57 +++++++ .../services/util/LocaleHelperTest.java | 23 --- .../services/util/LocaleNegotiatorTest.java | 49 ------ 17 files changed, 430 insertions(+), 232 deletions(-) create mode 100644 server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProvider.java create mode 100644 server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProviderFactory.java create mode 100644 server-spi/src/main/java/org/keycloak/locale/LocaleSelectorSPI.java create mode 100644 services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProvider.java create mode 100644 services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProviderFactory.java rename services/src/main/java/org/keycloak/{services/util => locale}/LocaleNegotiator.java (83%) create mode 100644 services/src/main/java/org/keycloak/locale/LocaleSelection.java delete mode 100755 services/src/main/java/org/keycloak/services/util/LocaleHelper.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.locale.LocaleSelectorProviderFactory create mode 100644 services/src/test/java/org/keycloak/locale/LocaleNegotiatorTest.java delete mode 100644 services/src/test/java/org/keycloak/services/util/LocaleHelperTest.java delete mode 100644 services/src/test/java/org/keycloak/services/util/LocaleNegotiatorTest.java diff --git a/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProvider.java b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProvider.java new file mode 100644 index 0000000000..a835dc4d05 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProvider.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.Provider; + +import java.util.Locale; + +public interface LocaleSelectorProvider extends Provider { + /** + * Resolve the locale which should be used for the request + * @param user + * @return + */ + Locale resolveLocale(RealmModel realm, UserModel user); +} diff --git a/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProviderFactory.java b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProviderFactory.java new file mode 100644 index 0000000000..c752478372 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorProviderFactory.java @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + +import org.keycloak.provider.ProviderFactory; + +public interface LocaleSelectorProviderFactory extends ProviderFactory { +} diff --git a/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorSPI.java b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorSPI.java new file mode 100644 index 0000000000..fa9d42baee --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/locale/LocaleSelectorSPI.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +public class LocaleSelectorSPI implements Spi { + + @Override + public boolean isInternal() { + return false; + } + + @Override + public String getName() { + return "localeSelector"; + } + + @Override + public Class getProviderClass() { + return LocaleSelectorProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return LocaleSelectorProviderFactory.class; + } + +} diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8fb53ab89e..c8ee6d107d 100755 --- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -32,6 +32,7 @@ # limitations under the License. # +org.keycloak.locale.LocaleSelectorSPI org.keycloak.storage.UserStorageProviderSpi org.keycloak.theme.ThemeResourceSpi org.keycloak.theme.ThemeSelectorSpi diff --git a/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProvider.java b/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProvider.java new file mode 100644 index 0000000000..756e23e84a --- /dev/null +++ b/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProvider.java @@ -0,0 +1,160 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + +import org.keycloak.OAuth2Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.util.CookieHelper; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriInfo; +import java.util.Locale; + +public class DefaultLocaleSelectorProvider implements LocaleSelectorProvider { + + protected static final String LOCALE_COOKIE = "KEYCLOAK_LOCALE"; + protected static final String KC_LOCALE_PARAM = "kc_locale"; + + protected final KeycloakSession session; + + public DefaultLocaleSelectorProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public Locale resolveLocale(RealmModel realm, UserModel user) { + final HttpHeaders requestHeaders = session.getContext().getRequestHeaders(); + final UriInfo uri = session.getContext().getUri(); + return getLocale(realm, user, requestHeaders, uri); + } + + @Override + public void close() { + + } + + protected Locale getLocale(RealmModel realm, UserModel user, HttpHeaders requestHeaders, UriInfo uriInfo) { + if (!realm.isInternationalizationEnabled()) { + return Locale.ENGLISH; + } else { + Locale locale = getUserLocale(realm, user, requestHeaders, uriInfo); + return locale != null ? locale : Locale.forLanguageTag(realm.getDefaultLocale()); + } + } + + protected Locale getUserLocale(RealmModel realm, UserModel user, HttpHeaders requestHeaders, UriInfo uriInfo) { + final LocaleSelection kcLocaleQueryParamSelection = getKcLocaleQueryParamSelection(realm, uriInfo); + if (kcLocaleQueryParamSelection != null) { + updateLocaleCookie(realm, kcLocaleQueryParamSelection.getLocaleString(), uriInfo); + if (user != null) { + updateUsersLocale(user, kcLocaleQueryParamSelection.getLocaleString()); + } + return kcLocaleQueryParamSelection.getLocale(); + } + + final LocaleSelection localeCookieSelection = getLocaleCookieSelection(realm, requestHeaders); + if (localeCookieSelection != null) { + if (user != null) { + updateUsersLocale(user, localeCookieSelection.getLocaleString()); + } + return localeCookieSelection.getLocale(); + } + + final LocaleSelection userProfileSelection = getUserProfileSelection(realm, user); + if (userProfileSelection != null) { + updateLocaleCookie(realm, userProfileSelection.getLocaleString(), uriInfo); + return userProfileSelection.getLocale(); + } + + final LocaleSelection uiLocalesQueryParamSelection = getUiLocalesQueryParamSelection(realm, uriInfo); + if (uiLocalesQueryParamSelection != null) { + return uiLocalesQueryParamSelection.getLocale(); + } + + final LocaleSelection acceptLanguageHeaderSelection = getAcceptLanguageHeaderLocale(realm, requestHeaders); + if (acceptLanguageHeaderSelection != null) { + return acceptLanguageHeaderSelection.getLocale(); + } + + return null; + } + + protected LocaleSelection getKcLocaleQueryParamSelection(RealmModel realm, UriInfo uriInfo) { + if (uriInfo == null || !uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)) { + return null; + } + String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM); + return findLocale(realm, localeString); + } + + protected LocaleSelection getLocaleCookieSelection(RealmModel realm, HttpHeaders httpHeaders) { + if (httpHeaders == null || !httpHeaders.getCookies().containsKey(LOCALE_COOKIE)) { + return null; + } + String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); + return findLocale(realm, localeString); + } + + protected LocaleSelection getUserProfileSelection(RealmModel realm, UserModel user) { + if (user == null || !user.getAttributes().containsKey(UserModel.LOCALE)) { + return null; + } + String localeString = user.getFirstAttribute(UserModel.LOCALE); + return findLocale(realm, localeString); + } + + protected LocaleSelection getUiLocalesQueryParamSelection(RealmModel realm, UriInfo uriInfo) { + if (uriInfo == null || !uriInfo.getQueryParameters().containsKey(OAuth2Constants.UI_LOCALES_PARAM)) { + return null; + } + String localeString = uriInfo.getQueryParameters().getFirst(OAuth2Constants.UI_LOCALES_PARAM); + return findLocale(realm, localeString.split(" ")); + } + + protected LocaleSelection getAcceptLanguageHeaderLocale(RealmModel realm, HttpHeaders httpHeaders) { + if (httpHeaders == null || httpHeaders.getAcceptableLanguages() == null || httpHeaders.getAcceptableLanguages().isEmpty()) { + return null; + } + for (Locale l : httpHeaders.getAcceptableLanguages()) { + String localeString = l.toLanguageTag(); + LocaleSelection localeSelection = findLocale(realm, localeString); + if (localeSelection != null) { + return localeSelection; + } + } + return null; + } + + protected void updateLocaleCookie(RealmModel realm, String locale, UriInfo uriInfo) { + boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost()); + CookieHelper.addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, secure, true); + } + + protected LocaleSelection findLocale(RealmModel realm, String... localeStrings) { + return new LocaleNegotiator(realm.getSupportedLocales()).invoke(localeStrings); + } + + protected void updateUsersLocale(UserModel user, String locale) { + if (!locale.equals(user.getFirstAttribute("locale"))) { + user.setSingleAttribute(UserModel.LOCALE, locale); + } + } + +} diff --git a/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProviderFactory.java b/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProviderFactory.java new file mode 100644 index 0000000000..864e30c4d4 --- /dev/null +++ b/services/src/main/java/org/keycloak/locale/DefaultLocaleSelectorProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class DefaultLocaleSelectorProviderFactory implements LocaleSelectorProviderFactory { + @Override + public LocaleSelectorProvider create(KeycloakSession session) { + return new DefaultLocaleSelectorProvider(session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "default"; + } +} diff --git a/services/src/main/java/org/keycloak/services/util/LocaleNegotiator.java b/services/src/main/java/org/keycloak/locale/LocaleNegotiator.java similarity index 83% rename from services/src/main/java/org/keycloak/services/util/LocaleNegotiator.java rename to services/src/main/java/org/keycloak/locale/LocaleNegotiator.java index 6553a77575..2d422856d8 100644 --- a/services/src/main/java/org/keycloak/services/util/LocaleNegotiator.java +++ b/services/src/main/java/org/keycloak/locale/LocaleNegotiator.java @@ -14,19 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.services.util; +package org.keycloak.locale; import java.util.Locale; import java.util.Set; -class LocaleNegotiator { +public class LocaleNegotiator { private Set supportedLocales; - LocaleNegotiator(Set supportedLocales) { + public LocaleNegotiator(Set supportedLocales) { this.supportedLocales = supportedLocales; } - Locale invoke(String... localeStrings) { + public LocaleSelection invoke(String... localeStrings) { for (String localeString : localeStrings) { if (localeString != null) { Locale result = null; @@ -38,15 +38,16 @@ class LocaleNegotiator { result = locale; } if (locale.getCountry().equals(search.getCountry())) { - return locale; + return new LocaleSelection(localeString, locale); } } } if (result != null) { - return result; + return new LocaleSelection(localeString, result); } } } return null; } + } diff --git a/services/src/main/java/org/keycloak/locale/LocaleSelection.java b/services/src/main/java/org/keycloak/locale/LocaleSelection.java new file mode 100644 index 0000000000..0630377a27 --- /dev/null +++ b/services/src/main/java/org/keycloak/locale/LocaleSelection.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.locale; + +import java.util.Locale; + +public class LocaleSelection { + private final String localeString; + private final Locale locale; + + public LocaleSelection(String localeString, Locale locale) { + this.localeString = localeString; + this.locale = locale; + } + + public String getLocaleString() { + return localeString; + } + + public Locale getLocale() { + return locale; + } +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java index 4f482effb7..47fb5c6f63 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -19,6 +19,7 @@ package org.keycloak.services; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.ClientConnection; +import org.keycloak.locale.LocaleSelectorProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; @@ -26,7 +27,6 @@ import org.keycloak.models.KeycloakUriInfo; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.resources.KeycloakApplication; -import org.keycloak.services.util.LocaleHelper; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; @@ -117,6 +117,6 @@ public class DefaultKeycloakContext implements KeycloakContext { @Override public Locale resolveLocale(UserModel user) { - return LocaleHelper.getLocale(session, realm, user); + return session.getProvider(LocaleSelectorProvider.class).resolveLocale(realm, user); } } diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java index 68c9786b6f..ba9058b454 100644 --- a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java +++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java @@ -11,7 +11,6 @@ import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.messages.Messages; -import org.keycloak.services.util.LocaleHelper; import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.Theme; import org.keycloak.theme.beans.LocaleBean; @@ -71,7 +70,7 @@ public class KeycloakErrorHandler implements ExceptionMapper { Theme theme = session.theme().getTheme(Theme.Type.LOGIN); - Locale locale = LocaleHelper.getLocale(session, realm, null); + Locale locale = session.getContext().resolveLocale(null); FreeMarkerUtil freeMarker = new FreeMarkerUtil(); Map attributes = initAttributes(realm, theme, locale, statusCode); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index d332fb79fe..d1d2221e72 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -17,7 +17,6 @@ import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.util.LocaleHelper; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; import org.keycloak.theme.BrowserSecurityHeaderSetup; @@ -100,7 +99,7 @@ public class AccountConsole { UserModel user = null; if (auth != null) user = auth.getUser(); - Locale locale = LocaleHelper.getLocale(session, realm, user); + Locale locale = session.getContext().resolveLocale(user); map.put("locale", locale.toLanguageTag()); Properties messages = theme.getMessages(locale); map.put("msg", new MessageFormatterMethod(locale, messages)); diff --git a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java deleted file mode 100755 index 49466bca68..0000000000 --- a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.keycloak.services.util; - -import org.keycloak.OAuth2Constants; -import org.keycloak.models.KeycloakContext; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.services.managers.AuthenticationManager; - -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.UriInfo; -import java.util.Locale; -import java.util.Set; - -/** - * @author Michael Gerber - */ -public class LocaleHelper { - - private static final String LOCALE_COOKIE = "KEYCLOAK_LOCALE"; - private static final String KC_LOCALE_PARAM = "kc_locale"; - - public static Locale getLocale(KeycloakSession session, RealmModel realm, UserModel user) { - if (!realm.isInternationalizationEnabled()) { - return Locale.ENGLISH; - } else { - Locale locale = getUserLocale(session, realm, user); - return locale != null ? locale : Locale.forLanguageTag(realm.getDefaultLocale()); - } - } - - public static Locale getLocaleFromCookie(KeycloakSession session) { - KeycloakContext ctx = session.getContext(); - - if (ctx.getRequestHeaders() != null && ctx.getRequestHeaders().getCookies().containsKey(LOCALE_COOKIE)) { - String localeString = ctx.getRequestHeaders().getCookies().get(LOCALE_COOKIE).getValue(); - Locale locale = findLocale(ctx.getRealm().getSupportedLocales(), localeString); - if (locale != null) { - return locale; - } - } - - String locale = ctx.getRealm().getDefaultLocale(); - if (locale != null) { - return Locale.forLanguageTag(locale); - } else { - return Locale.ENGLISH; - } - } - - private static Locale getUserLocale(KeycloakSession session, RealmModel realm, UserModel user) { - UriInfo uriInfo = session.getContext().getUri(); - HttpHeaders httpHeaders = session.getContext().getRequestHeaders(); - - // kc_locale query parameter - if (uriInfo != null && uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)) { - String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if (locale != null) { - updateLocaleCookie(session, realm, localeString); - if (user != null) { - updateUsersLocale(user, localeString); - } - return locale; - } - } - - // Locale cookie - if (httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)) { - String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if (locale != null) { - if (user != null) { - updateUsersLocale(user, localeString); - } - return locale; - } - } - - // User profile - if (user != null && user.getAttributes().containsKey(UserModel.LOCALE)) { - String localeString = user.getFirstAttribute(UserModel.LOCALE); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if (locale != null) { - updateLocaleCookie(session, realm, localeString); - return locale; - } - } - - // ui_locales query parameter - if (uriInfo != null && uriInfo.getQueryParameters().containsKey(OAuth2Constants.UI_LOCALES_PARAM)) { - String localeString = uriInfo.getQueryParameters().getFirst(OAuth2Constants.UI_LOCALES_PARAM); - Locale locale = findLocale(realm.getSupportedLocales(), localeString.split(" ")); - if (locale != null) { - return locale; - } - } - - // Accept-Language http header - if (httpHeaders != null && httpHeaders.getAcceptableLanguages() != null && !httpHeaders.getAcceptableLanguages().isEmpty()) { - for (Locale l : httpHeaders.getAcceptableLanguages()) { - String localeString = l.toLanguageTag(); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if (locale != null) { - return locale; - } - } - } - - return null; - } - - private static void updateLocaleCookie(KeycloakSession session, - RealmModel realm, - String locale) { - boolean secure = realm.getSslRequired().isRequired(session.getContext().getUri().getRequestUri().getHost()); - CookieHelper.addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, session.getContext().getUri()), null, null, -1, secure, true); - } - - private static Locale findLocale(Set supportedLocales, String... localeStrings) { - return new LocaleNegotiator(supportedLocales).invoke(localeStrings); - } - - private static void updateUsersLocale(UserModel user, String locale) { - if (!locale.equals(user.getFirstAttribute("locale"))) { - user.setSingleAttribute(UserModel.LOCALE, locale); - } - } - -} diff --git a/services/src/main/java/org/keycloak/services/util/P3PHelper.java b/services/src/main/java/org/keycloak/services/util/P3PHelper.java index 4579007396..61fcebcd5e 100644 --- a/services/src/main/java/org/keycloak/services/util/P3PHelper.java +++ b/services/src/main/java/org/keycloak/services/util/P3PHelper.java @@ -23,7 +23,6 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.services.validation.Validation; import org.keycloak.theme.Theme; -import org.keycloak.theme.ThemeProvider; import java.io.IOException; import java.util.Locale; @@ -41,7 +40,7 @@ public class P3PHelper { try { Theme theme = session.theme().getTheme(Theme.Type.LOGIN); - Locale locale = LocaleHelper.getLocaleFromCookie(session); + Locale locale = session.getContext().resolveLocale(null); String p3pValue = theme.getMessages(locale).getProperty("p3pPolicy"); if (!Validation.isBlank(p3pValue)) { diff --git a/services/src/main/resources/META-INF/services/org.keycloak.locale.LocaleSelectorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.locale.LocaleSelectorProviderFactory new file mode 100644 index 0000000000..14edd02273 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.locale.LocaleSelectorProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2016 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.locale.DefaultLocaleSelectorProviderFactory \ No newline at end of file diff --git a/services/src/test/java/org/keycloak/locale/LocaleNegotiatorTest.java b/services/src/test/java/org/keycloak/locale/LocaleNegotiatorTest.java new file mode 100644 index 0000000000..0da68d50c5 --- /dev/null +++ b/services/src/test/java/org/keycloak/locale/LocaleNegotiatorTest.java @@ -0,0 +1,57 @@ +package org.keycloak.locale; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public class LocaleNegotiatorTest { + + private LocaleNegotiator localeNegotiator; + + @Before + public void setUp() { + Set supportedLocales = new HashSet<>(); + supportedLocales.add("de"); + supportedLocales.add("de-AT"); + supportedLocales.add("de-CH"); + supportedLocales.add("de-DE"); + supportedLocales.add("pt-BR"); + localeNegotiator = new LocaleNegotiator(supportedLocales); + } + + @Test + public void shouldMatchWithoutCountryCode() { + String expectedLocaleString = "de"; + LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString); + Assert.assertEquals(Locale.GERMAN, actualLocale.getLocale()); + Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString()); + } + + @Test + public void shouldMatchWithPriorityCountryCode() { + String expectedLocaleString = "de-CH"; + LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "de"); + Assert.assertEquals(new Locale("de", "CH"), actualLocale.getLocale()); + Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString()); + } + + @Test + public void shouldMatchWithPriorityNoCountryCode() { + String expectedLocaleString = "de"; + LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "de-CH"); + Assert.assertEquals(new Locale(expectedLocaleString), actualLocale.getLocale()); + Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString()); + } + + @Test + public void shouldMatchOmittedCountryCodeWithBestFit() { + String expectedLocaleString = "pt"; + LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "es-ES"); + Assert.assertEquals(new Locale("pt", "BR"), actualLocale.getLocale()); + Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString()); + } +} diff --git a/services/src/test/java/org/keycloak/services/util/LocaleHelperTest.java b/services/src/test/java/org/keycloak/services/util/LocaleHelperTest.java deleted file mode 100644 index 573588693d..0000000000 --- a/services/src/test/java/org/keycloak/services/util/LocaleHelperTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.keycloak.services.util; - -import org.junit.Test; - -import java.lang.reflect.Method; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsNull.nullValue; - -public class LocaleHelperTest { - - @Test - public void shouldNotExceptionOnNullLocaleAttributeItem() throws Exception { - final Method method = LocaleHelper.class.getDeclaredMethod("findLocale", Set.class, String[].class); - method.setAccessible(true); - Locale foundLocale = (Locale) method.invoke(null, Stream.of("en", "es", "fr").collect(Collectors.toSet()), new String[]{null}); - assertThat(foundLocale, nullValue()); - } -} diff --git a/services/src/test/java/org/keycloak/services/util/LocaleNegotiatorTest.java b/services/src/test/java/org/keycloak/services/util/LocaleNegotiatorTest.java deleted file mode 100644 index 02aea7f8db..0000000000 --- a/services/src/test/java/org/keycloak/services/util/LocaleNegotiatorTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.keycloak.services.util; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; - -public class LocaleNegotiatorTest { - - private LocaleNegotiator localeNegotiator; - - @Before - public void setUp() { - Set supportedLocales = new HashSet<>(); - supportedLocales.add("de"); - supportedLocales.add("de-AT"); - supportedLocales.add("de-CH"); - supportedLocales.add("de-DE"); - supportedLocales.add("pt-BR"); - localeNegotiator = new LocaleNegotiator(supportedLocales); - } - - @Test - public void shouldMatchWithoutCountryCode() { - Locale actualLocale = localeNegotiator.invoke("de"); - Assert.assertEquals(Locale.GERMAN, actualLocale); - } - - @Test - public void shouldMatchWithPriorityCountryCode() { - Locale actualLocale = localeNegotiator.invoke("de-CH", "de"); - Assert.assertEquals(new Locale("de", "CH"), actualLocale); - } - - @Test - public void shouldMatchWithPriorityNoCountryCode() { - Locale actualLocale = localeNegotiator.invoke("de", "de-CH"); - Assert.assertEquals(new Locale("de"), actualLocale); - } - - @Test - public void shouldMatchOmittedCountryCodeWithBestFit() { - Locale actualLocale = localeNegotiator.invoke("pt", "es-ES"); - Assert.assertEquals(new Locale("pt", "BR"), actualLocale); - } -}