keycloak-uncached

KEYCLOAK-8146: Extract LocaleSelectorSPI to allow custom

8/25/2018 5:56:33 PM

Details

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 0000000..a835dc4
--- /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 0000000..c752478
--- /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<LocaleSelectorProvider> {
+}
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 0000000..fa9d42b
--- /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<? extends Provider> getProviderClass() {
+        return LocaleSelectorProvider.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> 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 8fb53ab..c8ee6d1 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 0000000..756e23e
--- /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 0000000..864e30c
--- /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/locale/LocaleSelection.java b/services/src/main/java/org/keycloak/locale/LocaleSelection.java
new file mode 100644
index 0000000..0630377
--- /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 4f482ef..47fb5c6 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 68c9786..ba9058b 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<Throwable> {
 
             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<String, Object> 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 d332fb7..d1d2221 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/P3PHelper.java b/services/src/main/java/org/keycloak/services/util/P3PHelper.java
index 4579007..61fcebc 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 0000000..14edd02
--- /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