diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml index 24902d56da..9d6e545b5a 100644 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml @@ -8,6 +8,10 @@ + + + + diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java index 020445e406..7ad8c6e443 100755 --- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java @@ -11,6 +11,7 @@ public class ClientRepresentation { protected String id; protected String clientId; protected String name; + protected String rootUrl; protected String adminUrl; protected String baseUrl; protected Boolean surrogateAuthRequired; @@ -74,6 +75,14 @@ public class ClientRepresentation { this.surrogateAuthRequired = surrogateAuthRequired; } + public String getRootUrl() { + return rootUrl; + } + + public void setRootUrl(String rootUrl) { + this.rootUrl = rootUrl; + } + public String getAdminUrl() { return adminUrl; } diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml index 6b39bf664f..c702020844 100755 --- a/docbook/reference/en/en-US/modules/server-installation.xml +++ b/docbook/reference/en/en-US/modules/server-installation.xml @@ -843,27 +843,25 @@ All configuration options are optional. Default value for directory is
Installing Keycloak Server as Root Context - The Keycloak server can be installed as the default web application. This way, instead of referencing - the server as http://mydomain.com/auth, it would be - http://mydomain.com/. + The Keycloak server can be installed as the default web application. In doing so, the server can be referenced at http://mydomain.com/ instead of http://mydomain.com/auth. - To do this, you need to add a default-web-module attribute in the Undertow subystem in standalone.xml. + To do this, add the default-web-module attribute in the Undertow subystem in standalone.xml. + - + ]]> - main-auth-server is the name of the Keycloak server as defined in the Keycloak subsystem. + keycloak-server.war is the runtime name of the Keycloak server application. Note that the WAR file does not exist as a file. If its name changes (ie. keycloak-server.war) in the future, find its new name from the Keycloak log entry with runtime-name:. - If you have already run your server before changing to the root context then your database - will contain references to the old /auth context. And, your clients may also have incorrect + If you have run your server before altering the root context, your database + will contain references to the old /auth context. Your clients may also have incorrect references. To fix this on the server side, you will need to export your database to json, make corrections, and then import. Client-side keycloak.json files will need to be updated manually as well. diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml index cd59f382ad..10b2417a46 100755 --- a/docbook/reference/en/en-US/modules/themes.xml +++ b/docbook/reference/en/en-US/modules/themes.xml @@ -181,6 +181,12 @@ import=common/keycloak messages/messages.properties inside your theme folder and add the following content: username=Your Username + + For the admin console, there is a second resource bundle named admin-messages.properties. + This resource bundle is converted to JSON and shipped to the console to be processed by + angular-translate. It is found in the same directory as messages.properties and can be overridden + in the same way as described above. +
Modifying HTML diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java index 35081ce7b9..a4b3771761 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.account.freemarker; import java.io.IOException; @@ -197,7 +213,10 @@ public class FreeMarkerAccountProvider implements AccountProvider { String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri,realm.getName())); + + String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName()); + + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -209,7 +228,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { this.passwordSet = passwordSet; return this; } - + protected void setMessage(MessageType type, String message, Object... parameters) { messageType = type; messages = new ArrayList<>(); @@ -225,7 +244,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { return message.getMessage(); } } - + @Override public AccountProvider setErrors(List messages) { this.messageType = MessageType.ERROR; diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java index 0e98f1147f..020d349150 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java @@ -224,10 +224,15 @@ public class ExtendingThemeManager implements ThemeProvider { @Override public Properties getMessages(Locale locale) throws IOException { + return getMessages("messages", locale); + } + + @Override + public Properties getMessages(String baseBundlename, Locale locale) throws IOException { Properties messages = new Properties(); ListIterator itr = themes.listIterator(themes.size()); while (itr.hasPrevious()) { - Properties m = itr.previous().getMessages(locale); + Properties m = itr.previous().getMessages(baseBundlename, locale); if (m != null) { messages.putAll(m); } diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java index 9f1aafe4e4..14f7f6c6aa 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java @@ -1,6 +1,21 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.freemarker; -import org.jboss.logging.Logger; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -38,7 +53,7 @@ public class LocaleHelper { return locale; } } - + //1. Locale cookie if(httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)){ String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); @@ -89,7 +104,11 @@ public class LocaleHelper { return Locale.ENGLISH; } - public static void updateLocaleCookie(Response.ResponseBuilder builder, Locale locale, RealmModel realm, UriInfo uriInfo, String path) { + public static void updateLocaleCookie(Response.ResponseBuilder builder, + Locale locale, + RealmModel realm, + UriInfo uriInfo, + String path) { if (locale == null) { return; } diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java index 6a12a49f00..43107ce723 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java @@ -29,8 +29,27 @@ public interface Theme { public InputStream getResourceAsStream(String path) throws IOException; + /** + * Same as getMessages(baseBundlename, locale), but uses a default baseBundlename + * such as "messages". + * + * @param locale The locale of the desired message bundle. + * @return The localized messages from the bundle. + * @throws IOException If bundle can not be read. + */ public Properties getMessages(Locale locale) throws IOException; + /** + * Retrieve localized messages from a message bundle. + * + * @param baseBundlename The base name of the bundle, such as "messages" in + * messages_en.properties. + * @param locale The locale of the desired message bundle. + * @return The localized messages from the bundle. + * @throws IOException If bundle can not be read. + */ + public Properties getMessages(String baseBundlename, Locale locale) throws IOException; + public Properties getProperties() throws IOException; } diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java index 3e92be880b..68ac7caca1 100755 --- a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java @@ -100,12 +100,17 @@ public class ClassLoaderTheme implements Theme { @Override public Properties getMessages(Locale locale) throws IOException { + return getMessages("messages", locale); + } + + @Override + public Properties getMessages(String baseBundlename, Locale locale) throws IOException { if(locale == null){ return null; } Properties m = new Properties(); - URL url = classLoader.getResource(this.messageRoot + "messages_" + locale.toString() + ".properties"); + URL url = classLoader.getResource(this.messageRoot + baseBundlename + "_" + locale.toString() + ".properties"); if (url != null) { m.load(url.openStream()); } diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java index d1593fe99d..77a65a8e59 100644 --- a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java @@ -93,13 +93,18 @@ public class FolderTheme implements Theme { @Override public Properties getMessages(Locale locale) throws IOException { + return getMessages("messages", locale); + } + + @Override + public Properties getMessages(String baseBundlename, Locale locale) throws IOException { if(locale == null){ return null; } Properties m = new Properties(); - File file = new File(themeDir, "messages" + File.separator + "messages_" + locale.toString() + ".properties"); + File file = new File(themeDir, "messages" + File.separator + baseBundlename + "_" + locale.toString() + ".properties"); if (file.isFile()) { m.load(new FileInputStream(file)); } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl index 1a87bce79d..1584d0d93a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/index.ftl +++ b/forms/common-themes/src/main/resources/theme/base/admin/index.ftl @@ -21,6 +21,10 @@ + + + + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties new file mode 100644 index 0000000000..6442e6e05a --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties @@ -0,0 +1,116 @@ +# Common messages +enabled=de Enabled +name=de Name +save=de Save +cancel=de Cancel +onText=AN +offText=AUS +client=de Client +clear=de Clear + +# Realm settings +realm-detail.enabled.tooltip=de Users and clients can only access a realm if it's enabled +registrationAllowed=de User registration +registrationAllowed.tooltip=de Enable/disable the registration page. A link for registration will show on login page too. +registrationEmailAsUsername=de Email as username +registrationEmailAsUsername.tooltip=de If enabled then username field is hidden from registration form and email is used as username for new user. +editUsernameAllowed=de Edit username +editUsernameAllowed.tooltip=de If enabled, the username field is editable, readonly otherwise. +resetPasswordAllowed=de Forget password +resetPasswordAllowed.tooltip=de Show a link on login page for user to click on when they have forgotten their credentials. +rememberMe=de Remember Me +rememberMe.tooltip=de Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. +verifyEmail=de Verify email +verifyEmail.tooltip=de Require the user to verify their email address the first time they login. +sslRequired=de Require SSL +sslRequired.option.all=de all requests +sslRequired.option.external=de external requests +sslRequired.option.none=de none +sslRequired.tooltip=de Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses. +publicKey=de Public key +gen-new-keys=de Generate new keys +certificate=de Certificate +host=de Host +smtp-host=de SMTP Host +port=de Port +smtp-port=de SMTP Port (defaults to 25) +from=de From +sender-email-addr=de Sender Email Address +enable-ssl=de Enable SSL +enable-start-tls=de Enable StartTLS +enable-auth=de Enable Authentication +username=de Username +login-username=de Login Username +password=de Password +login-password=de Login Password +login-theme=de Login Theme +select-one=de Select one... +login-theme.tooltip=de Select theme for login, TOTP, grant, registration, and forgot password pages. +account-theme=de Account Theme +account-theme.tooltip=de Select theme for user account management pages. +admin-console-theme=de Admin Console Theme +select-theme-admin-console=de Select theme for admin console. +email-theme=de Email Theme +select-theme-email=de Select theme for emails that are sent by the server. +i18n-enabled=de Internationalization Enabled +supported-locales=de Supported Locales +supported-locales.placeholder=de Type a locale and enter +default-locale=de Default Locale +realm-cache-enabled=de Realm Cache Enabled +realm-cache-enabled.tooltip=de Enable/disable cache for realm, client and role data. +user-cache-enabled=de User Cache Enabled +user-cache-enabled.tooltip=de Enable/disable user and user role mapping cache. +sso-session-idle=de SSO Session Idle +seconds=de Seconds +minutes=de Minutes +hours=de Hours +days=de Days +sso-session-max=de SSO Session Max +sso-session-idle.tooltip=de Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired. +sso-session-max.tooltip=de Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired. +access-token-lifespan=de Access Token Lifespan +access-token-lifespan.tooltip=de Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout. +client-login-timeout=de Client login timeout +client-login-timeout.tooltip=de Max time an client has to finish the access token protocol. This should normally be 1 minute. +login-timeout=de Login timeout +login-timeout.tooltip=de Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more. +login-action-timeout=de Login action timeout +login-action-timeout.tooltip=de Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more. +headers=de Headers +brute-force-detection=de Brute Force Detection +x-frame-options=de X-Frame-Options +click-label-for-info=de Click on label link for more information. The default value prevents pages from being included via non-origin iframes. +content-sec-policy=de Content-Security-Policy +max-login-failures=de Max Login Failures +max-login-failures.tooltip=de How many failures before wait is triggered. +wait-increment=de Wait Increment +wait-increment.tooltip=de When failure threshold has been met, how much time should the user be locked out? +quick-login-check-millis=de Quick Login Check Milli Seconds +quick-login-check-millis.tooltip=de If a failure happens concurrently too quickly, lock out the user. +min-quick-login-wait=de Minimum Quick Login Wait +min-quick-login-wait.tooltip=de How long to wait after a quick login failure. +max-wait=de Max Wait +max-wait.tooltip=de Max time a user will be locked out. +failure-reset-time=de Failure Reset Time +failure-reset-time.tooltip=de When will failure count be reset? +realm-tab-login=de Login +realm-tab-keys=de Keys +realm-tab-email=de Email +realm-tab-themes=de Themes +realm-tab-cache=de Cache +realm-tab-tokens=de Tokens +realm-tab-security-defenses=de Security Defenses +realm-tab-general=de General +add-realm=de Add Realm + +#Session settings +realm-sessions=de Realm Sessions +revocation=de Revocation +logout-all=de Logout All +active-sessions=de Active Sessions +sessions=de Sessions +not-before=de Not Before +not-before.tooltip=de Revoke any tokens issued before this date. +set-to-now=de Set To Now +push=de Push +push.tooltip=de For every client that has an admin URL, notify them of the new revocation policy. diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties new file mode 100644 index 0000000000..be3ef2de22 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -0,0 +1,116 @@ +# Common messages +enabled=Enabled +name=Name +save=Save +cancel=Cancel +onText=ON +offText=OFF +client=Client +clear=Clear + +# Realm settings +realm-detail.enabled.tooltip=Users and clients can only access a realm if it's enabled +registrationAllowed=User registration +registrationAllowed.tooltip=Enable/disable the registration page. A link for registration will show on login page too. +registrationEmailAsUsername=Email as username +registrationEmailAsUsername.tooltip=If enabled then username field is hidden from registration form and email is used as username for new user. +editUsernameAllowed=Edit username +editUsernameAllowed.tooltip=If enabled, the username field is editable, readonly otherwise. +resetPasswordAllowed=Forget password +resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials. +rememberMe=Remember Me +rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. +verifyEmail=Verify email +verifyEmail.tooltip=Require the user to verify their email address the first time they login. +sslRequired=Require SSL +sslRequired.option.all=all requests +sslRequired.option.external=external requests +sslRequired.option.none=none +sslRequired.tooltip=Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses. +publicKey=Public key +gen-new-keys=Generate new keys +certificate=Certificate +host=Host +smtp-host=SMTP Host +port=Port +smtp-port=SMTP Port (defaults to 25) +from=From +sender-email-addr=Sender Email Address +enable-ssl=Enable SSL +enable-start-tls=Enable StartTLS +enable-auth=Enable Authentication +username=Username +login-username=Login Username +password=Password +login-password=Login Password +login-theme=Login Theme +select-one=Select one... +login-theme.tooltip=Select theme for login, TOTP, grant, registration, and forgot password pages. +account-theme=Account Theme +account-theme.tooltip=Select theme for user account management pages. +admin-console-theme=Admin Console Theme +select-theme-admin-console=Select theme for admin console. +email-theme=Email Theme +select-theme-email=Select theme for emails that are sent by the server. +i18n-enabled=Internationalization Enabled +supported-locales=Supported Locales +supported-locales.placeholder=Type a locale and enter +default-locale=Default Locale +realm-cache-enabled=Realm Cache Enabled +realm-cache-enabled.tooltip=Enable/disable cache for realm, client and role data. +user-cache-enabled=User Cache Enabled +user-cache-enabled.tooltip=Enable/disable user and user role mapping cache. +sso-session-idle=SSO Session Idle +seconds=Seconds +minutes=Minutes +hours=Hours +days=Days +sso-session-max=SSO Session Max +sso-session-idle.tooltip=Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired. +sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired. +access-token-lifespan=Access Token Lifespan +access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout. +client-login-timeout=Client login timeout +client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute. +login-timeout=Login timeout +login-timeout.tooltip=Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more. +login-action-timeout=Login action timeout +login-action-timeout.tooltip=Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more. +headers=Headers +brute-force-detection=Brute Force Detection +x-frame-options=X-Frame-Options +click-label-for-info=Click on label link for more information. The default value prevents pages from being included via non-origin iframes. +content-sec-policy=Content-Security-Policy +max-login-failures=Max Login Failures +max-login-failures.tooltip=How many failures before wait is triggered. +wait-increment=Wait Increment +wait-increment.tooltip=When failure threshold has been met, how much time should the user be locked out? +quick-login-check-millis=Quick Login Check Milli Seconds +quick-login-check-millis.tooltip=If a failure happens concurrently too quickly, lock out the user. +min-quick-login-wait=Minimum Quick Login Wait +min-quick-login-wait.tooltip=How long to wait after a quick login failure. +max-wait=Max Wait +max-wait.tooltip=Max time a user will be locked out. +failure-reset-time=Failure Reset Time +failure-reset-time.tooltip=When will failure count be reset? +realm-tab-login=Login +realm-tab-keys=Keys +realm-tab-email=Email +realm-tab-themes=Themes +realm-tab-cache=Cache +realm-tab-tokens=Tokens +realm-tab-security-defenses=Security Defenses +realm-tab-general=General +add-realm=Add Realm + +#Session settings +realm-sessions=Realm Sessions +revocation=Revocation +logout-all=Logout All +active-sessions=Active Sessions +sessions=Sessions +not-before=Not Before +not-before.tooltip=Revoke any tokens issued before this date. +set-to-now=Set To Now +push=Push +push.tooltip=For every client that has an admin URL, notify them of the new revocation policy. diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 756c89e2ac..350f0d0315 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -7,7 +7,7 @@ var configUrl = consoleBaseUrl + "/config"; var auth = {}; -var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload' ]); +var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'pascalprecht.translate', 'ngCookies', 'ngSanitize']); var resourceRequests = 0; var loadingTimer = -1; @@ -52,8 +52,18 @@ module.factory('authInterceptor', function($q, Auth) { }; }); - - +module.config(['$translateProvider', function($translateProvider) { + $translateProvider.useSanitizeValueStrategy('sanitizeParameters'); + + var locale = auth.authz.idTokenParsed.locale; + if (locale !== undefined) { + $translateProvider.preferredLanguage(locale); + } else { + $translateProvider.preferredLanguage('en'); + } + + $translateProvider.useUrlLoader('messages.json'); +}]); module.config([ '$routeProvider', function($routeProvider) { $routeProvider diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index ae334e73c6..3cb3e6029f 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -564,8 +564,6 @@ module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role, module.controller('ClientImportCtrl', function($scope, $location, $upload, realm, serverInfo, Notifications) { $scope.realm = realm; - $scope.configFormats = serverInfo.clientImporters; - $scope.configFormat = null; $scope.files = []; @@ -614,7 +612,6 @@ module.controller('ClientImportCtrl', function($scope, $location, $upload, realm module.controller('ClientListCtrl', function($scope, realm, clients, Client, serverInfo, $route, Dialog, Notifications) { $scope.realm = realm; $scope.clients = clients; - $scope.importButton = serverInfo.clientImporters.length > 0; $scope.removeClient = function(client) { Dialog.confirmDelete(client.clientId, 'client', function() { @@ -670,7 +667,7 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie } }); -module.controller('ClientDetailCtrl', function($scope, realm, client, $route, serverInfo, Client, $location, Dialog, Notifications) { +module.controller('ClientDetailCtrl', function($scope, realm, client, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) { $scope.accessTypes = [ "confidential", "public", @@ -709,40 +706,45 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se $scope.samlEncrypt = false; $scope.samlForcePostBinding = false; $scope.samlForceNameIdFormat = false; - if (!$scope.create) { - if (!client.attributes) { - client.attributes = {}; + + function updateProperties() { + if (!$scope.client.attributes) { + $scope.client.attributes = {}; } - $scope.client= angular.copy(client); $scope.accessType = $scope.accessTypes[0]; - if (client.bearerOnly) { + if ($scope.client.bearerOnly) { $scope.accessType = $scope.accessTypes[2]; - } else if (client.publicClient) { + } else if ($scope.client.publicClient) { $scope.accessType = $scope.accessTypes[1]; } - if (client.protocol) { - $scope.protocol = $scope.protocols[$scope.protocols.indexOf(client.protocol)]; + if ($scope.client.protocol) { + $scope.protocol = $scope.protocols[$scope.protocols.indexOf($scope.client.protocol)]; } else { $scope.protocol = $scope.protocols[0]; } - if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA1') { + if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA1') { $scope.signatureAlgorithm = $scope.signatureAlgorithms[0]; - } else if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA256') { + } else if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA256') { $scope.signatureAlgorithm = $scope.signatureAlgorithms[1]; - } else if (client.attributes['saml.signature.algorithm'] == 'RSA_SHA512') { + } else if ($scope.client.attributes['saml.signature.algorithm'] == 'RSA_SHA512') { $scope.signatureAlgorithm = $scope.signatureAlgorithms[2]; - } else if (client.attributes['saml.signature.algorithm'] == 'DSA_SHA1') { + } else if ($scope.client.attributes['saml.signature.algorithm'] == 'DSA_SHA1') { $scope.signatureAlgorithm = $scope.signatureAlgorithms[3]; } - if (client.attributes['saml_name_id_format'] == 'unspecified') { + if ($scope.client.attributes['saml_name_id_format'] == 'unspecified') { $scope.nameIdFormat = $scope.nameIdFormats[0]; - } else if (client.attributes['saml_name_id_format'] == 'email') { + } else if ($scope.client.attributes['saml_name_id_format'] == 'email') { $scope.nameIdFormat = $scope.nameIdFormats[1]; - } else if (client.attributes['saml_name_id_format'] == 'transient') { + } else if ($scope.client.attributes['saml_name_id_format'] == 'transient') { $scope.nameIdFormat = $scope.nameIdFormats[2]; - } else if (client.attributes['saml_name_id_format'] == 'persistent') { + } else if ($scope.client.attributes['saml_name_id_format'] == 'persistent') { $scope.nameIdFormat = $scope.nameIdFormats[3]; } + } + + if (!$scope.create) { + $scope.client = angular.copy(client); + updateProperties(); } else { $scope.client = { enabled: true, attributes: {}}; $scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value; @@ -813,6 +815,29 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, $route, se } } + $scope.importFile = function(fileContent){ + console.debug(fileContent); + ClientDescriptionConverter.save({ + realm: realm.realm + }, fileContent, function (data) { + $scope.client = data; + updateProperties(); + $scope.importing = true; + }); + }; + + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'JsonModalCtrl', + resolve: { + object: function () { + return $scope.client; + } + } + }) + }; + $scope.switchChange = function() { $scope.changed = true; } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index d1abe26cd6..8d56c90750 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -927,6 +927,13 @@ module.factory('Client', function($resource) { }); }); +module.factory('ClientDescriptionConverter', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/client-description-converter', { + realm : '@realm' + }); +}); + + module.factory('ClientInstallation', function($resource) { var url = authUrl + '/admin/realms/:realm/clients/:client/installation/json'; return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html index e26e126c70..33845c0b40 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html @@ -2,102 +2,102 @@
- +
- +
- +
- How many failures before wait is triggered. + {{:: 'max-login-failures.tooltip' | translate}}
- +
- When failure threshold has been met, how much time should the user be locked out? + {{:: 'wait-increment.tooltip' | translate}}
- +
- If a failure happens concurrently too quickly, lock out the user. + {{:: 'quick-login-check-millis.tooltip' | translate}}
- +
- How long to wait after a quick login failure. + {{:: 'min-quick-login-wait.tooltip' | translate}}
- +
- Max time a user will be locked out. + {{:: 'max-wait.tooltip' | translate}}
- +
- When will failure count be reset? + {{:: 'failure-reset-time.tooltip' | translate}}
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 06c939bc4c..cc4ea82622 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -10,6 +10,20 @@
+
+ + +
+ + +
+ +
+ + +
+
+
@@ -174,6 +188,14 @@ The name ID format to use for the subject.
+
+ +
+ +
+ Root URL appended to relative URLs +
+
@@ -252,7 +274,7 @@
- Allowed CORS origins. + Allowed CORS origins. To permit all origins of Valid Redirect URIs add '+'. To permit all origins add '*'.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html index 1b194bbb16..dfa3e001fa 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html @@ -37,7 +37,7 @@ {{client.clientId}} {{client.enabled}} - {{client.baseUrl}} + {{client.rootUrl}}{{client.baseUrl}} Not defined diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html index c6bd5b7605..aa1dc4e29f 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html @@ -2,31 +2,31 @@
- +
- Click on label link for more information. The default value prevents pages from being included via non-origin iframes. + {{:: 'click-label-for-info' | translate}}
- +
- Click on label link for more information. The default value prevents pages from being included via non-origin iframes. + {{:: 'click-label-for-info' | translate}}
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html index c7f9b26210..db267969e9 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-cache-settings.html @@ -3,24 +3,24 @@
- +
- +
- Enable/disable cache for realm, client and role data. + {{:: 'realm-cache-enabled.tooltip' | translate}}
- +
- +
- Enable/disable user and user role mapping cache. + {{:: 'user-cache-enabled.tooltip' | translate}}
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html index 4ac36fd7cb..6de9285b35 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-create.html @@ -1,8 +1,9 @@
+

Add Realm

+
- Create Realm
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html index 469e8c62d2..d39ba1403b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html @@ -3,29 +3,29 @@
- +
- +
- +
- Users and clients can only access a realm if it's enabled + {{:: 'realm-detail.enabled.tooltip' | translate}}
- - + +
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html index ddda54049e..93301af67a 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-keys.html @@ -4,7 +4,7 @@
- +
@@ -22,7 +22,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html index adccf3bee6..9766c7a649 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html @@ -4,66 +4,66 @@
- +
- +
- Enable/disable the registration page. A link for registration will show on login page too. + {{:: 'registrationAllowed.tooltip' | translate}}
- +
- +
- If enabled then username field is hidden from registration form and email is used as username for new user. + {{:: 'registrationEmailAsUsername.tooltip' | translate}}
- +
- +
- If enabled, the username field is editable, readonly otherwise. + {{:: 'editUsernameAllowed.tooltip' | translate}}
- +
- +
- Show a link on login page for user to click on when they have forgotten their credentials. + {{:: 'resetPasswordAllowed.tooltip' |translate}}
- +
- +
- Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. + {{:: 'rememberMe.tooltip' | translate}}
- +
- +
- Require the user to verify their email address the first time they login. + {{:: 'verifyEmail.tooltip' | translate}}
- +
- Is HTTPS required? 'None' means HTTPS is not required for any client IP address. 'External requests' means localhost and private IP addresses can access without HTTPS. 'All requests' means HTTPS is required for all IP addresses. + {{:: 'sslRequired.tooltip' | translate}}
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html index be703d4b5c..f9b5f6b5cc 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-smtp.html @@ -3,59 +3,59 @@
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html index caecc6dcb8..4d77b89d87 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-theme-settings.html @@ -4,72 +4,72 @@
- +
- Select theme for login, TOTP, grant, registration, and forgot password pages. + {{:: 'login-theme.tooltip' | translate}}
- +
- Select theme for user account management pages. + {{ 'account-theme.tooltip' | translate}}
- +
- Select theme for admin console. + {{:: 'select-theme-admin-console' | translate}}
- +
- Select theme for emails that are sent by the server. + {{:: 'select-theme-email' | translate}}
- +
- +
- +
- +
- +
@@ -86,8 +86,8 @@
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html index 56ed0d74fb..a44b939b99 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html @@ -4,110 +4,110 @@
- +
- Time a session is allowed to be idle before it expires. Tokens and browser sessions are invalidated when a session is expired. + {{:: 'sso-session-idle.tooltip' | translate}}
- +
- Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired. + {{:: 'sso-session-max.tooltip' | translate}}
- +
- Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout. + {{:: 'access-token-lifespan.tooltip' | translate}}
- +
- Max time an client has to finish the access token protocol. This should normally be 1 minute. + {{:: 'client-login-timeout.tooltip' | translate}}
- +
- Max time a user has to complete a login. This is recommended to be relatively long. 30 minutes or more. + {{:: 'login-timeout' | translate}}
- +
- Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long. 5 minutes or more. + {{:: 'login-action-timeout.tooltip' | translate}}
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html index 5410f09e6a..e724d952d1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-realm.html @@ -1,9 +1,9 @@
-

Sessions

+

{{:: 'sessions' | translate}}

@@ -11,13 +11,13 @@ - - + + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html index 52296e4a10..03c6864582 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/session-revocation.html @@ -1,27 +1,27 @@
-

Sessions

+

{{:: 'sessions' | translate}}

- +
- Revoke any tokens issued before this date. + {{:: 'not-before.tooltip' | translate}}
- - - + + +
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html index 892570d881..76f01eafb5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html @@ -3,16 +3,16 @@ {{realm.realm|capitalize}} -

Add Realm

+

{{:: 'add-realm' | translate}}

\ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js new file mode 100644 index 0000000000..619a819dd4 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-loader-url.js @@ -0,0 +1,75 @@ +/*! + * angular-translate - v2.7.2 - 2015-06-01 + * http://github.com/angular-translate/angular-translate + * Copyright (c) 2015 ; Licensed MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define([], function () { + return (factory()); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + factory(); + } +}(this, function () { + +angular.module('pascalprecht.translate') +/** + * @ngdoc object + * @name pascalprecht.translate.$translateUrlLoader + * @requires $q + * @requires $http + * + * @description + * Creates a loading function for a typical dynamic url pattern: + * "locale.php?lang=en_US", "locale.php?lang=de_DE", "locale.php?language=nl_NL" etc. + * Prefixing the specified url, the current requested, language id will be applied + * with "?{queryParameter}={key}". + * Using this service, the response of these urls must be an object of + * key-value pairs. + * + * @param {object} options Options object, which gets the url, key and + * optional queryParameter ('lang' is used by default). + */ +.factory('$translateUrlLoader', $translateUrlLoader); + +function $translateUrlLoader($q, $http) { + + 'use strict'; + + return function (options) { + + if (!options || !options.url) { + throw new Error('Couldn\'t use urlLoader since no url is given!'); + } + + var deferred = $q.defer(), + requestParams = {}; + + requestParams[options.queryParameter || 'lang'] = options.key; + + $http(angular.extend({ + url: options.url, + params: requestParams, + method: 'GET' + }, options.$http)).success(function (data) { + deferred.resolve(data); + }).error(function () { + deferred.reject(options.key); + }); + + return deferred.promise; + }; +} +$translateUrlLoader.$inject = ['$q', '$http']; + +$translateUrlLoader.displayName = '$translateUrlLoader'; +return 'pascalprecht.translate'; + +})); diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js new file mode 100644 index 0000000000..3ac40605c0 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate-storage-cookie.js @@ -0,0 +1,71 @@ +/*! + * angular-translate - v2.6.1 - 2015-03-01 + * http://github.com/angular-translate/angular-translate + * Copyright (c) 2015 ; Licensed MIT + */ +angular.module('pascalprecht.translate') + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateCookieStorage + * @requires $cookieStore + * + * @description + * Abstraction layer for cookieStore. This service is used when telling angular-translate + * to use cookieStore as storage. + * + */ +.factory('$translateCookieStorage', ['$cookieStore', function ($cookieStore) { + + var $translateCookieStorage = { + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateCookieStorage#get + * @methodOf pascalprecht.translate.$translateCookieStorage + * + * @description + * Returns an item from cookieStorage by given name. + * + * @param {string} name Item name + * @return {string} Value of item name + */ + get: function (name) { + return $cookieStore.get(name); + }, + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateCookieStorage#set + * @methodOf pascalprecht.translate.$translateCookieStorage + * + * @description + * Sets an item in cookieStorage by given name. + * + * @deprecated use #put + * + * @param {string} name Item name + * @param {string} value Item value + */ + set: function (name, value) { + $cookieStore.put(name, value); + }, + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateCookieStorage#put + * @methodOf pascalprecht.translate.$translateCookieStorage + * + * @description + * Sets an item in cookieStorage by given name. + * + * @param {string} name Item name + * @param {string} value Item value + */ + put: function (name, value) { + $cookieStore.put(name, value); + } + }; + + return $translateCookieStorage; +}]); diff --git a/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js new file mode 100644 index 0000000000..e7183a0a9f --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/keycloak/common/resources/lib/angular/angular-translate.js @@ -0,0 +1,2904 @@ +/*! + * angular-translate - v2.7.2 - 2015-06-01 + * http://github.com/angular-translate/angular-translate + * Copyright (c) 2015 ; Licensed MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define([], function () { + return (factory()); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + factory(); + } +}(this, function () { + +/** + * @ngdoc overview + * @name pascalprecht.translate + * + * @description + * The main module which holds everything together. + */ +angular.module('pascalprecht.translate', ['ng']) + .run(runTranslate); + +function runTranslate($translate) { + + 'use strict'; + + var key = $translate.storageKey(), + storage = $translate.storage(); + + var fallbackFromIncorrectStorageValue = function () { + var preferred = $translate.preferredLanguage(); + if (angular.isString(preferred)) { + $translate.use(preferred); + // $translate.use() will also remember the language. + // So, we don't need to call storage.put() here. + } else { + storage.put(key, $translate.use()); + } + }; + + fallbackFromIncorrectStorageValue.displayName = 'fallbackFromIncorrectStorageValue'; + + if (storage) { + if (!storage.get(key)) { + fallbackFromIncorrectStorageValue(); + } else { + $translate.use(storage.get(key))['catch'](fallbackFromIncorrectStorageValue); + } + } else if (angular.isString($translate.preferredLanguage())) { + $translate.use($translate.preferredLanguage()); + } +} +runTranslate.$inject = ['$translate']; + +runTranslate.displayName = 'runTranslate'; + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateSanitizationProvider + * + * @description + * + * Configurations for $translateSanitization + */ +angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider); + +function $translateSanitizationProvider () { + + 'use strict'; + + var $sanitize, + currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0. + hasConfiguredStrategy = false, + hasShownNoStrategyConfiguredWarning = false, + strategies; + + /** + * Definition of a sanitization strategy function + * @callback StrategyFunction + * @param {string|object} value - value to be sanitized (either a string or an interpolated value map) + * @param {string} mode - either 'text' for a string (translation) or 'params' for the interpolated params + * @return {string|object} + */ + + /** + * @ngdoc property + * @name strategies + * @propertyOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Following strategies are built-in: + *
+ *
sanitize
+ *
Sanitizes HTML in the translation text using $sanitize
+ *
escape
+ *
Escapes HTML in the translation
+ *
sanitizeParameters
+ *
Sanitizes HTML in the values of the interpolation parameters using $sanitize
+ *
escapeParameters
+ *
Escapes HTML in the values of the interpolation parameters
+ *
escaped
+ *
Support legacy strategy name 'escaped' for backwards compatibility (will be removed in 3.0)
+ *
+ * + */ + + strategies = { + sanitize: function (value, mode) { + if (mode === 'text') { + value = htmlSanitizeValue(value); + } + return value; + }, + escape: function (value, mode) { + if (mode === 'text') { + value = htmlEscapeValue(value); + } + return value; + }, + sanitizeParameters: function (value, mode) { + if (mode === 'params') { + value = mapInterpolationParameters(value, htmlSanitizeValue); + } + return value; + }, + escapeParameters: function (value, mode) { + if (mode === 'params') { + value = mapInterpolationParameters(value, htmlEscapeValue); + } + return value; + } + }; + // Support legacy strategy name 'escaped' for backwards compatibility. + // TODO should be removed in 3.0 + strategies.escaped = strategies.escapeParameters; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#addStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Adds a sanitization strategy to the list of known strategies. + * + * @param {string} strategyName - unique key for a strategy + * @param {StrategyFunction} strategyFunction - strategy function + * @returns {object} this + */ + this.addStrategy = function (strategyName, strategyFunction) { + strategies[strategyName] = strategyFunction; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#removeStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Removes a sanitization strategy from the list of known strategies. + * + * @param {string} strategyName - unique key for a strategy + * @returns {object} this + */ + this.removeStrategy = function (strategyName) { + delete strategies[strategyName]; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitizationProvider#useStrategy + * @methodOf pascalprecht.translate.$translateSanitizationProvider + * + * @description + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * + * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + * @returns {object} this + */ + this.useStrategy = function (strategy) { + hasConfiguredStrategy = true; + currentStrategy = strategy; + return this; + }; + + /** + * @ngdoc object + * @name pascalprecht.translate.$translateSanitization + * @requires $injector + * @requires $log + * + * @description + * Sanitizes interpolation parameters and translated texts. + * + */ + this.$get = ['$injector', '$log', function ($injector, $log) { + + var applyStrategies = function (value, mode, selectedStrategies) { + angular.forEach(selectedStrategies, function (selectedStrategy) { + if (angular.isFunction(selectedStrategy)) { + value = selectedStrategy(value, mode); + } else if (angular.isFunction(strategies[selectedStrategy])) { + value = strategies[selectedStrategy](value, mode); + } else { + throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + selectedStrategy + '\''); + } + }); + return value; + }; + + // TODO: should be removed in 3.0 + var showNoStrategyConfiguredWarning = function () { + if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) { + $log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.'); + hasShownNoStrategyConfiguredWarning = true; + } + }; + + if ($injector.has('$sanitize')) { + $sanitize = $injector.get('$sanitize'); + } + + return { + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitization#useStrategy + * @methodOf pascalprecht.translate.$translateSanitization + * + * @description + * Selects a sanitization strategy. When an array is provided the strategies will be executed in order. + * + * @param {string|StrategyFunction|array} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions. + */ + useStrategy: (function (self) { + return function (strategy) { + self.useStrategy(strategy); + }; + })(this), + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateSanitization#sanitize + * @methodOf pascalprecht.translate.$translateSanitization + * + * @description + * Sanitizes a value. + * + * @param {string|object} value The value which should be sanitized. + * @param {string} mode The current sanitization mode, either 'params' or 'text'. + * @param {string|StrategyFunction|array} [strategy] Optional custom strategy which should be used instead of the currently selected strategy. + * @returns {string|object} sanitized value + */ + sanitize: function (value, mode, strategy) { + if (!currentStrategy) { + showNoStrategyConfiguredWarning(); + } + + if (arguments.length < 3) { + strategy = currentStrategy; + } + + if (!strategy) { + return value; + } + + var selectedStrategies = angular.isArray(strategy) ? strategy : [strategy]; + return applyStrategies(value, mode, selectedStrategies); + } + }; + }]; + + var htmlEscapeValue = function (value) { + var element = angular.element('
'); + element.text(value); // not chainable, see #1044 + return element.html(); + }; + + var htmlSanitizeValue = function (value) { + if (!$sanitize) { + throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.'); + } + return $sanitize(value); + }; + + var mapInterpolationParameters = function (value, iteratee) { + if (angular.isObject(value)) { + var result = angular.isArray(value) ? [] : {}; + + angular.forEach(value, function (propertyValue, propertyKey) { + result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee); + }); + + return result; + } else if (angular.isNumber(value)) { + return value; + } else { + return iteratee(value); + } + }; +} + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateProvider + * @description + * + * $translateProvider allows developers to register translation-tables, asynchronous loaders + * and similar to configure translation behavior directly inside of a module. + * + */ +angular.module('pascalprecht.translate') +.constant('pascalprechtTranslateOverrider', {}) +.provider('$translate', $translate); + +function $translate($STORAGE_KEY, $windowProvider, $translateSanitizationProvider, pascalprechtTranslateOverrider) { + + 'use strict'; + + var $translationTable = {}, + $preferredLanguage, + $availableLanguageKeys = [], + $languageKeyAliases, + $fallbackLanguage, + $fallbackWasString, + $uses, + $nextLang, + $storageFactory, + $storageKey = $STORAGE_KEY, + $storagePrefix, + $missingTranslationHandlerFactory, + $interpolationFactory, + $interpolatorFactories = [], + $loaderFactory, + $cloakClassName = 'translate-cloak', + $loaderOptions, + $notFoundIndicatorLeft, + $notFoundIndicatorRight, + $postCompilingEnabled = false, + $forceAsyncReloadEnabled = false, + NESTED_OBJECT_DELIMITER = '.', + loaderCache, + directivePriority = 0, + statefulFilter = true, + uniformLanguageTagResolver = 'default', + languageTagResolver = { + 'default': function (tag) { + return (tag || '').split('-').join('_'); + }, + java: function (tag) { + var temp = (tag || '').split('-').join('_'); + var parts = temp.split('_'); + return parts.length > 1 ? (parts[0].toLowerCase() + '_' + parts[1].toUpperCase()) : temp; + }, + bcp47: function (tag) { + var temp = (tag || '').split('_').join('-'); + var parts = temp.split('-'); + return parts.length > 1 ? (parts[0].toLowerCase() + '-' + parts[1].toUpperCase()) : temp; + } + }; + + var version = '2.7.2'; + + // tries to determine the browsers language + var getFirstBrowserLanguage = function () { + + // internal purpose only + if (angular.isFunction(pascalprechtTranslateOverrider.getLocale)) { + return pascalprechtTranslateOverrider.getLocale(); + } + + var nav = $windowProvider.$get().navigator, + browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'], + i, + language; + + // support for HTML 5.1 "navigator.languages" + if (angular.isArray(nav.languages)) { + for (i = 0; i < nav.languages.length; i++) { + language = nav.languages[i]; + if (language && language.length) { + return language; + } + } + } + + // support for other well known properties in browsers + for (i = 0; i < browserLanguagePropertyKeys.length; i++) { + language = nav[browserLanguagePropertyKeys[i]]; + if (language && language.length) { + return language; + } + } + + return null; + }; + getFirstBrowserLanguage.displayName = 'angular-translate/service: getFirstBrowserLanguage'; + + // tries to determine the browsers locale + var getLocale = function () { + var locale = getFirstBrowserLanguage() || ''; + if (languageTagResolver[uniformLanguageTagResolver]) { + locale = languageTagResolver[uniformLanguageTagResolver](locale); + } + return locale; + }; + getLocale.displayName = 'angular-translate/service: getLocale'; + + /** + * @name indexOf + * @private + * + * @description + * indexOf polyfill. Kinda sorta. + * + * @param {array} array Array to search in. + * @param {string} searchElement Element to search for. + * + * @returns {int} Index of search element. + */ + var indexOf = function(array, searchElement) { + for (var i = 0, len = array.length; i < len; i++) { + if (array[i] === searchElement) { + return i; + } + } + return -1; + }; + + /** + * @name trim + * @private + * + * @description + * trim polyfill + * + * @returns {string} The string stripped of whitespace from both ends + */ + var trim = function() { + return this.toString().replace(/^\s+|\s+$/g, ''); + }; + + var negotiateLocale = function (preferred) { + + var avail = [], + locale = angular.lowercase(preferred), + i = 0, + n = $availableLanguageKeys.length; + + for (; i < n; i++) { + avail.push(angular.lowercase($availableLanguageKeys[i])); + } + + if (indexOf(avail, locale) > -1) { + return preferred; + } + + if ($languageKeyAliases) { + var alias; + for (var langKeyAlias in $languageKeyAliases) { + var hasWildcardKey = false; + var hasExactKey = Object.prototype.hasOwnProperty.call($languageKeyAliases, langKeyAlias) && + angular.lowercase(langKeyAlias) === angular.lowercase(preferred); + + if (langKeyAlias.slice(-1) === '*') { + hasWildcardKey = langKeyAlias.slice(0, -1) === preferred.slice(0, langKeyAlias.length-1); + } + if (hasExactKey || hasWildcardKey) { + alias = $languageKeyAliases[langKeyAlias]; + if (indexOf(avail, angular.lowercase(alias)) > -1) { + return alias; + } + } + } + } + + if (preferred) { + var parts = preferred.split('_'); + + if (parts.length > 1 && indexOf(avail, angular.lowercase(parts[0])) > -1) { + return parts[0]; + } + } + + // If everything fails, just return the preferred, unchanged. + return preferred; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#translations + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a new translation table for specific language key. + * + * To register a translation table for specific language, pass a defined language + * key as first parameter. + * + *
+   *  // register translation table for language: 'de_DE'
+   *  $translateProvider.translations('de_DE', {
+   *    'GREETING': 'Hallo Welt!'
+   *  });
+   *
+   *  // register another one
+   *  $translateProvider.translations('en_US', {
+   *    'GREETING': 'Hello world!'
+   *  });
+   * 
+ * + * When registering multiple translation tables for for the same language key, + * the actual translation table gets extended. This allows you to define module + * specific translation which only get added, once a specific module is loaded in + * your app. + * + * Invoking this method with no arguments returns the translation table which was + * registered with no language key. Invoking it with a language key returns the + * related translation table. + * + * @param {string} key A language key. + * @param {object} translationTable A plain old JavaScript object that represents a translation table. + * + */ + var translations = function (langKey, translationTable) { + + if (!langKey && !translationTable) { + return $translationTable; + } + + if (langKey && !translationTable) { + if (angular.isString(langKey)) { + return $translationTable[langKey]; + } + } else { + if (!angular.isObject($translationTable[langKey])) { + $translationTable[langKey] = {}; + } + angular.extend($translationTable[langKey], flatObject(translationTable)); + } + return this; + }; + + this.translations = translations; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#cloakClassName + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * + * Let's you change the class name for `translate-cloak` directive. + * Default class name is `translate-cloak`. + * + * @param {string} name translate-cloak class name + */ + this.cloakClassName = function (name) { + if (!name) { + return $cloakClassName; + } + $cloakClassName = name; + return this; + }; + + /** + * @name flatObject + * @private + * + * @description + * Flats an object. This function is used to flatten given translation data with + * namespaces, so they are later accessible via dot notation. + */ + var flatObject = function (data, path, result, prevKey) { + var key, keyWithPath, keyWithShortPath, val; + + if (!path) { + path = []; + } + if (!result) { + result = {}; + } + for (key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + val = data[key]; + if (angular.isObject(val)) { + flatObject(val, path.concat(key), result, key); + } else { + keyWithPath = path.length ? ('' + path.join(NESTED_OBJECT_DELIMITER) + NESTED_OBJECT_DELIMITER + key) : key; + if(path.length && key === prevKey){ + // Create shortcut path (foo.bar == foo.bar.bar) + keyWithShortPath = '' + path.join(NESTED_OBJECT_DELIMITER); + // Link it to original path + result[keyWithShortPath] = '@:' + keyWithPath; + } + result[keyWithPath] = val; + } + } + return result; + }; + flatObject.displayName = 'flatObject'; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#addInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Adds interpolation services to angular-translate, so it can manage them. + * + * @param {object} factory Interpolation service factory + */ + this.addInterpolation = function (factory) { + $interpolatorFactories.push(factory); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMessageFormatInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use interpolation functionality of messageformat.js. + * This is useful when having high level pluralization and gender selection. + */ + this.useMessageFormatInterpolation = function () { + return this.useInterpolation('$translateMessageFormatInterpolation'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useInterpolation + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate which interpolation style to use as default, application-wide. + * Simply pass a factory/service name. The interpolation service has to implement + * the correct interface. + * + * @param {string} factory Interpolation service name. + */ + this.useInterpolation = function (factory) { + $interpolationFactory = factory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useSanitizeStrategy + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Simply sets a sanitation strategy type. + * + * @param {string} value Strategy type. + */ + this.useSanitizeValueStrategy = function (value) { + $translateSanitizationProvider.useStrategy(value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#preferredLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which of the registered translation tables to use for translation + * at initial startup by passing a language key. Similar to `$translateProvider#use` + * only that it says which language to **prefer**. + * + * @param {string} langKey A language key. + * + */ + this.preferredLanguage = function(langKey) { + setupPreferredLanguage(langKey); + return this; + + }; + var setupPreferredLanguage = function (langKey) { + if (langKey) { + $preferredLanguage = langKey; + } + return $preferredLanguage; + }; + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicator + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found. E.g. when + * setting the indicator as 'X' and one tries to translate a translation id + * called `NOT_FOUND`, this will result in `X NOT_FOUND X`. + * + * Internally this methods sets a left indicator and a right indicator using + * `$translateProvider.translationNotFoundIndicatorLeft()` and + * `$translateProvider.translationNotFoundIndicatorRight()`. + * + * **Note**: These methods automatically add a whitespace between the indicators + * and the translation id. + * + * @param {string} indicator An indicator, could be any string. + */ + this.translationNotFoundIndicator = function (indicator) { + this.translationNotFoundIndicatorLeft(indicator); + this.translationNotFoundIndicatorRight(indicator); + return this; + }; + + /** + * ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found left to the + * translation id. + * + * @param {string} indicator An indicator. + */ + this.translationNotFoundIndicatorLeft = function (indicator) { + if (!indicator) { + return $notFoundIndicatorLeft; + } + $notFoundIndicatorLeft = indicator; + return this; + }; + + /** + * ngdoc function + * @name pascalprecht.translate.$translateProvider#translationNotFoundIndicatorLeft + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets an indicator which is used when a translation isn't found right to the + * translation id. + * + * @param {string} indicator An indicator. + */ + this.translationNotFoundIndicatorRight = function (indicator) { + if (!indicator) { + return $notFoundIndicatorRight; + } + $notFoundIndicatorRight = indicator; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#fallbackLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which of the registered translation tables to use when missing translations + * at initial startup by passing a language key. Similar to `$translateProvider#use` + * only that it says which language to **fallback**. + * + * @param {string||array} langKey A language key. + * + */ + this.fallbackLanguage = function (langKey) { + fallbackStack(langKey); + return this; + }; + + var fallbackStack = function (langKey) { + if (langKey) { + if (angular.isString(langKey)) { + $fallbackWasString = true; + $fallbackLanguage = [ langKey ]; + } else if (angular.isArray(langKey)) { + $fallbackWasString = false; + $fallbackLanguage = langKey; + } + if (angular.isString($preferredLanguage) && indexOf($fallbackLanguage, $preferredLanguage) < 0) { + $fallbackLanguage.push($preferredLanguage); + } + + return this; + } else { + if ($fallbackWasString) { + return $fallbackLanguage[0]; + } else { + return $fallbackLanguage; + } + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#use + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Set which translation table to use for translation by given language key. When + * trying to 'use' a language which isn't provided, it'll throw an error. + * + * You actually don't have to use this method since `$translateProvider#preferredLanguage` + * does the job too. + * + * @param {string} langKey A language key. + */ + this.use = function (langKey) { + if (langKey) { + if (!$translationTable[langKey] && (!$loaderFactory)) { + // only throw an error, when not loading translation data asynchronously + throw new Error('$translateProvider couldn\'t find translationTable for langKey: \'' + langKey + '\''); + } + $uses = langKey; + return this; + } + return $uses; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#storageKey + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells the module which key must represent the choosed language by a user in the storage. + * + * @param {string} key A key for the storage. + */ + var storageKey = function(key) { + if (!key) { + if ($storagePrefix) { + return $storagePrefix + $storageKey; + } + return $storageKey; + } + $storageKey = key; + return this; + }; + + this.storageKey = storageKey; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useUrlLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateUrlLoader` extension service as loader. + * + * @param {string} url Url + * @param {Object=} options Optional configuration object + */ + this.useUrlLoader = function (url, options) { + return this.useLoader('$translateUrlLoader', angular.extend({ url: url }, options)); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader. + * + * @param {Object=} options Optional configuration object + */ + this.useStaticFilesLoader = function (options) { + return this.useLoader('$translateStaticFilesLoader', options); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLoader + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use any other service as loader. + * + * @param {string} loaderFactory Factory name to use + * @param {Object=} options Optional configuration object + */ + this.useLoader = function (loaderFactory, options) { + $loaderFactory = loaderFactory; + $loaderOptions = options || {}; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLocalStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateLocalStorage` service as storage layer. + * + */ + this.useLocalStorage = function () { + return this.useStorage('$translateLocalStorage'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useCookieStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use `$translateCookieStorage` service as storage layer. + */ + this.useCookieStorage = function () { + return this.useStorage('$translateCookieStorage'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useStorage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use custom service as storage layer. + */ + this.useStorage = function (storageFactory) { + $storageFactory = storageFactory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#storagePrefix + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets prefix for storage key. + * + * @param {string} prefix Storage key prefix + */ + this.storagePrefix = function (prefix) { + if (!prefix) { + return prefix; + } + $storagePrefix = prefix; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandlerLog + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to use built-in log handler when trying to translate + * a translation Id which doesn't exist. + * + * This is actually a shortcut method for `useMissingTranslationHandler()`. + * + */ + this.useMissingTranslationHandlerLog = function () { + return this.useMissingTranslationHandler('$translateMissingTranslationHandlerLog'); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useMissingTranslationHandler + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Expects a factory name which later gets instantiated with `$injector`. + * This method can be used to tell angular-translate to use a custom + * missingTranslationHandler. Just build a factory which returns a function + * and expects a translation id as argument. + * + * Example: + *
+   *  app.config(function ($translateProvider) {
+   *    $translateProvider.useMissingTranslationHandler('customHandler');
+   *  });
+   *
+   *  app.factory('customHandler', function (dep1, dep2) {
+   *    return function (translationId) {
+   *      // something with translationId and dep1 and dep2
+   *    };
+   *  });
+   * 
+ * + * @param {string} factory Factory name + */ + this.useMissingTranslationHandler = function (factory) { + $missingTranslationHandlerFactory = factory; + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#usePostCompiling + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * If post compiling is enabled, all translated values will be processed + * again with AngularJS' $compile. + * + * Example: + *
+   *  app.config(function ($translateProvider) {
+   *    $translateProvider.usePostCompiling(true);
+   *  });
+   * 
+ * + * @param {string} factory Factory name + */ + this.usePostCompiling = function (value) { + $postCompilingEnabled = !(!value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#forceAsyncReload + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * If force async reload is enabled, async loader will always be called + * even if $translationTable already contains the language key, adding + * possible new entries to the $translationTable. + * + * Example: + *
+   *  app.config(function ($translateProvider) {
+   *    $translateProvider.forceAsyncReload(true);
+   *  });
+   * 
+ * + * @param {boolean} value - valid values are true or false + */ + this.forceAsyncReload = function (value) { + $forceAsyncReloadEnabled = !(!value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#uniformLanguageTag + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate which language tag should be used as a result when determining + * the current browser language. + * + * This setting must be set before invoking {@link pascalprecht.translate.$translateProvider#methods_determinePreferredLanguage determinePreferredLanguage()}. + * + *
+   * $translateProvider
+   *   .uniformLanguageTag('bcp47')
+   *   .determinePreferredLanguage()
+   * 
+ * + * The resolver currently supports: + * * default + * (traditionally: hyphens will be converted into underscores, i.e. en-US => en_US) + * en-US => en_US + * en_US => en_US + * en-us => en_us + * * java + * like default, but the second part will be always in uppercase + * en-US => en_US + * en_US => en_US + * en-us => en_US + * * BCP 47 (RFC 4646 & 4647) + * en-US => en-US + * en_US => en-US + * en-us => en-US + * + * See also: + * * http://en.wikipedia.org/wiki/IETF_language_tag + * * http://www.w3.org/International/core/langtags/ + * * http://tools.ietf.org/html/bcp47 + * + * @param {string|object} options - options (or standard) + * @param {string} options.standard - valid values are 'default', 'bcp47', 'java' + */ + this.uniformLanguageTag = function (options) { + + if (!options) { + options = {}; + } else if (angular.isString(options)) { + options = { + standard: options + }; + } + + uniformLanguageTagResolver = options.standard; + + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#determinePreferredLanguage + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Tells angular-translate to try to determine on its own which language key + * to set as preferred language. When `fn` is given, angular-translate uses it + * to determine a language key, otherwise it uses the built-in `getLocale()` + * method. + * + * The `getLocale()` returns a language key in the format `[lang]_[country]` or + * `[lang]` depending on what the browser provides. + * + * Use this method at your own risk, since not all browsers return a valid + * locale (see {@link pascalprecht.translate.$translateProvider#methods_uniformLanguageTag uniformLanguageTag()}). + * + * @param {Function=} fn Function to determine a browser's locale + */ + this.determinePreferredLanguage = function (fn) { + + var locale = (fn && angular.isFunction(fn)) ? fn() : getLocale(); + + if (!$availableLanguageKeys.length) { + $preferredLanguage = locale; + } else { + $preferredLanguage = negotiateLocale(locale); + } + + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#registerAvailableLanguageKeys + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a set of language keys the app will work with. Use this method in + * combination with + * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. + * When available languages keys are registered, angular-translate + * tries to find the best fitting language key depending on the browsers locale, + * considering your language key convention. + * + * @param {object} languageKeys Array of language keys the your app will use + * @param {object=} aliases Alias map. + */ + this.registerAvailableLanguageKeys = function (languageKeys, aliases) { + if (languageKeys) { + $availableLanguageKeys = languageKeys; + if (aliases) { + $languageKeyAliases = aliases; + } + return this; + } + return $availableLanguageKeys; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#useLoaderCache + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Registers a cache for internal $http based loaders. + * {@link pascalprecht.translate.$translateProvider#determinePreferredLanguage determinePreferredLanguage}. + * When false the cache will be disabled (default). When true or undefined + * the cache will be a default (see $cacheFactory). When an object it will + * be treat as a cache object itself: the usage is $http({cache: cache}) + * + * @param {object} cache boolean, string or cache-object + */ + this.useLoaderCache = function (cache) { + if (cache === false) { + // disable cache + loaderCache = undefined; + } else if (cache === true) { + // enable cache using AJS defaults + loaderCache = true; + } else if (typeof(cache) === 'undefined') { + // enable cache using default + loaderCache = '$translationCache'; + } else if (cache) { + // enable cache using given one (see $cacheFactory) + loaderCache = cache; + } + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#directivePriority + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Sets the default priority of the translate directive. The standard value is `0`. + * Calling this function without an argument will return the current value. + * + * @param {number} priority for the translate-directive + */ + this.directivePriority = function (priority) { + if (priority === undefined) { + // getter + return directivePriority; + } else { + // setter with chaining + directivePriority = priority; + return this; + } + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateProvider#statefulFilter + * @methodOf pascalprecht.translate.$translateProvider + * + * @description + * Since AngularJS 1.3, filters which are not stateless (depending at the scope) + * have to explicit define this behavior. + * Sets whether the translate filter should be stateful or stateless. The standard value is `true` + * meaning being stateful. + * Calling this function without an argument will return the current value. + * + * @param {boolean} state - defines the state of the filter + */ + this.statefulFilter = function (state) { + if (state === undefined) { + // getter + return statefulFilter; + } else { + // setter with chaining + statefulFilter = state; + return this; + } + }; + + /** + * @ngdoc object + * @name pascalprecht.translate.$translate + * @requires $interpolate + * @requires $log + * @requires $rootScope + * @requires $q + * + * @description + * The `$translate` service is the actual core of angular-translate. It expects a translation id + * and optional interpolate parameters to translate contents. + * + *
+   *  $translate('HEADLINE_TEXT').then(function (translation) {
+   *    $scope.translatedText = translation;
+   *  });
+   * 
+ * + * @param {string|array} translationId A token which represents a translation id + * This can be optionally an array of translation ids which + * results that the function returns an object where each key + * is the translation id and the value the translation. + * @param {object=} interpolateParams An object hash for dynamic values + * @param {string} interpolationId The id of the interpolation to use + * @returns {object} promise + */ + this.$get = [ + '$log', + '$injector', + '$rootScope', + '$q', + function ($log, $injector, $rootScope, $q) { + + var Storage, + defaultInterpolator = $injector.get($interpolationFactory || '$translateDefaultInterpolation'), + pendingLoader = false, + interpolatorHashMap = {}, + langPromises = {}, + fallbackIndex, + startFallbackIteration; + + var $translate = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { + + // Duck detection: If the first argument is an array, a bunch of translations was requested. + // The result is an object. + if (angular.isArray(translationId)) { + // Inspired by Q.allSettled by Kris Kowal + // https://github.com/kriskowal/q/blob/b0fa72980717dc202ffc3cbf03b936e10ebbb9d7/q.js#L1553-1563 + // This transforms all promises regardless resolved or rejected + var translateAll = function (translationIds) { + var results = {}; // storing the actual results + var promises = []; // promises to wait for + // Wraps the promise a) being always resolved and b) storing the link id->value + var translate = function (translationId) { + var deferred = $q.defer(); + var regardless = function (value) { + results[translationId] = value; + deferred.resolve([translationId, value]); + }; + // we don't care whether the promise was resolved or rejected; just store the values + $translate(translationId, interpolateParams, interpolationId, defaultTranslationText).then(regardless, regardless); + return deferred.promise; + }; + for (var i = 0, c = translationIds.length; i < c; i++) { + promises.push(translate(translationIds[i])); + } + // wait for all (including storing to results) + return $q.all(promises).then(function () { + // return the results + return results; + }); + }; + return translateAll(translationId); + } + + var deferred = $q.defer(); + + // trim off any whitespace + if (translationId) { + translationId = trim.apply(translationId); + } + + var promiseToWaitFor = (function () { + var promise = $preferredLanguage ? + langPromises[$preferredLanguage] : + langPromises[$uses]; + + fallbackIndex = 0; + + if ($storageFactory && !promise) { + // looks like there's no pending promise for $preferredLanguage or + // $uses. Maybe there's one pending for a language that comes from + // storage. + var langKey = Storage.get($storageKey); + promise = langPromises[langKey]; + + if ($fallbackLanguage && $fallbackLanguage.length) { + var index = indexOf($fallbackLanguage, langKey); + // maybe the language from storage is also defined as fallback language + // we increase the fallback language index to not search in that language + // as fallback, since it's probably the first used language + // in that case the index starts after the first element + fallbackIndex = (index === 0) ? 1 : 0; + + // but we can make sure to ALWAYS fallback to preferred language at least + if (indexOf($fallbackLanguage, $preferredLanguage) < 0) { + $fallbackLanguage.push($preferredLanguage); + } + } + } + return promise; + }()); + + if (!promiseToWaitFor) { + // no promise to wait for? okay. Then there's no loader registered + // nor is a one pending for language that comes from storage. + // We can just translate. + determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); + } else { + var promiseResolved = function () { + determineTranslation(translationId, interpolateParams, interpolationId, defaultTranslationText).then(deferred.resolve, deferred.reject); + }; + promiseResolved.displayName = 'promiseResolved'; + + promiseToWaitFor['finally'](promiseResolved, deferred.reject); + } + return deferred.promise; + }; + + /** + * @name applyNotFoundIndicators + * @private + * + * @description + * Applies not fount indicators to given translation id, if needed. + * This function gets only executed, if a translation id doesn't exist, + * which is why a translation id is expected as argument. + * + * @param {string} translationId Translation id. + * @returns {string} Same as given translation id but applied with not found + * indicators. + */ + var applyNotFoundIndicators = function (translationId) { + // applying notFoundIndicators + if ($notFoundIndicatorLeft) { + translationId = [$notFoundIndicatorLeft, translationId].join(' '); + } + if ($notFoundIndicatorRight) { + translationId = [translationId, $notFoundIndicatorRight].join(' '); + } + return translationId; + }; + + /** + * @name useLanguage + * @private + * + * @description + * Makes actual use of a language by setting a given language key as used + * language and informs registered interpolators to also use the given + * key as locale. + * + * @param {key} Locale key. + */ + var useLanguage = function (key) { + $uses = key; + $rootScope.$emit('$translateChangeSuccess', {language: key}); + + if ($storageFactory) { + Storage.put($translate.storageKey(), $uses); + } + // inform default interpolator + defaultInterpolator.setLocale($uses); + + var eachInterpolator = function (interpolator, id) { + interpolatorHashMap[id].setLocale($uses); + }; + eachInterpolator.displayName = 'eachInterpolatorLocaleSetter'; + + // inform all others too! + angular.forEach(interpolatorHashMap, eachInterpolator); + $rootScope.$emit('$translateChangeEnd', {language: key}); + }; + + /** + * @name loadAsync + * @private + * + * @description + * Kicks of registered async loader using `$injector` and applies existing + * loader options. When resolved, it updates translation tables accordingly + * or rejects with given language key. + * + * @param {string} key Language key. + * @return {Promise} A promise. + */ + var loadAsync = function (key) { + if (!key) { + throw 'No language key specified for loading.'; + } + + var deferred = $q.defer(); + + $rootScope.$emit('$translateLoadingStart', {language: key}); + pendingLoader = true; + + var cache = loaderCache; + if (typeof(cache) === 'string') { + // getting on-demand instance of loader + cache = $injector.get(cache); + } + + var loaderOptions = angular.extend({}, $loaderOptions, { + key: key, + $http: angular.extend({}, { + cache: cache + }, $loaderOptions.$http) + }); + + var onLoaderSuccess = function (data) { + var translationTable = {}; + $rootScope.$emit('$translateLoadingSuccess', {language: key}); + + if (angular.isArray(data)) { + angular.forEach(data, function (table) { + angular.extend(translationTable, flatObject(table)); + }); + } else { + angular.extend(translationTable, flatObject(data)); + } + pendingLoader = false; + deferred.resolve({ + key: key, + table: translationTable + }); + $rootScope.$emit('$translateLoadingEnd', {language: key}); + }; + onLoaderSuccess.displayName = 'onLoaderSuccess'; + + var onLoaderError = function (key) { + $rootScope.$emit('$translateLoadingError', {language: key}); + deferred.reject(key); + $rootScope.$emit('$translateLoadingEnd', {language: key}); + }; + onLoaderError.displayName = 'onLoaderError'; + + $injector.get($loaderFactory)(loaderOptions) + .then(onLoaderSuccess, onLoaderError); + + return deferred.promise; + }; + + if ($storageFactory) { + Storage = $injector.get($storageFactory); + + if (!Storage.get || !Storage.put) { + throw new Error('Couldn\'t use storage \'' + $storageFactory + '\', missing get() or put() method!'); + } + } + + // if we have additional interpolations that were added via + // $translateProvider.addInterpolation(), we have to map'em + if ($interpolatorFactories.length) { + var eachInterpolationFactory = function (interpolatorFactory) { + var interpolator = $injector.get(interpolatorFactory); + // setting initial locale for each interpolation service + interpolator.setLocale($preferredLanguage || $uses); + // make'em recognizable through id + interpolatorHashMap[interpolator.getInterpolationIdentifier()] = interpolator; + }; + eachInterpolationFactory.displayName = 'interpolationFactoryAdder'; + + angular.forEach($interpolatorFactories, eachInterpolationFactory); + } + + /** + * @name getTranslationTable + * @private + * + * @description + * Returns a promise that resolves to the translation table + * or is rejected if an error occurred. + * + * @param langKey + * @returns {Q.promise} + */ + var getTranslationTable = function (langKey) { + var deferred = $q.defer(); + if (Object.prototype.hasOwnProperty.call($translationTable, langKey)) { + deferred.resolve($translationTable[langKey]); + } else if (langPromises[langKey]) { + var onResolve = function (data) { + translations(data.key, data.table); + deferred.resolve(data.table); + }; + onResolve.displayName = 'translationTableResolver'; + langPromises[langKey].then(onResolve, deferred.reject); + } else { + deferred.reject(); + } + return deferred.promise; + }; + + /** + * @name getFallbackTranslation + * @private + * + * @description + * Returns a promise that will resolve to the translation + * or be rejected if no translation was found for the language. + * This function is currently only used for fallback language translation. + * + * @param langKey The language to translate to. + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} + */ + var getFallbackTranslation = function (langKey, translationId, interpolateParams, Interpolator) { + var deferred = $q.defer(); + + var onResolve = function (translationTable) { + if (Object.prototype.hasOwnProperty.call(translationTable, translationId)) { + Interpolator.setLocale(langKey); + var translation = translationTable[translationId]; + if (translation.substr(0, 2) === '@:') { + getFallbackTranslation(langKey, translation.substr(2), interpolateParams, Interpolator) + .then(deferred.resolve, deferred.reject); + } else { + deferred.resolve(Interpolator.interpolate(translationTable[translationId], interpolateParams)); + } + Interpolator.setLocale($uses); + } else { + deferred.reject(); + } + }; + onResolve.displayName = 'fallbackTranslationResolver'; + + getTranslationTable(langKey).then(onResolve, deferred.reject); + + return deferred.promise; + }; + + /** + * @name getFallbackTranslationInstant + * @private + * + * @description + * Returns a translation + * This function is currently only used for fallback language translation. + * + * @param langKey The language to translate to. + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {string} translation + */ + var getFallbackTranslationInstant = function (langKey, translationId, interpolateParams, Interpolator) { + var result, translationTable = $translationTable[langKey]; + + if (translationTable && Object.prototype.hasOwnProperty.call(translationTable, translationId)) { + Interpolator.setLocale(langKey); + result = Interpolator.interpolate(translationTable[translationId], interpolateParams); + if (result.substr(0, 2) === '@:') { + return getFallbackTranslationInstant(langKey, result.substr(2), interpolateParams, Interpolator); + } + Interpolator.setLocale($uses); + } + + return result; + }; + + + /** + * @name translateByHandler + * @private + * + * Translate by missing translation handler. + * + * @param translationId + * @returns translation created by $missingTranslationHandler or translationId is $missingTranslationHandler is + * absent + */ + var translateByHandler = function (translationId, interpolateParams) { + // If we have a handler factory - we might also call it here to determine if it provides + // a default text for a translationid that can't be found anywhere in our tables + if ($missingTranslationHandlerFactory) { + var resultString = $injector.get($missingTranslationHandlerFactory)(translationId, $uses, interpolateParams); + if (resultString !== undefined) { + return resultString; + } else { + return translationId; + } + } else { + return translationId; + } + }; + + /** + * @name resolveForFallbackLanguage + * @private + * + * Recursive helper function for fallbackTranslation that will sequentially look + * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. + * + * @param fallbackLanguageIndex + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} Promise that will resolve to the translation. + */ + var resolveForFallbackLanguage = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator, defaultTranslationText) { + var deferred = $q.defer(); + + if (fallbackLanguageIndex < $fallbackLanguage.length) { + var langKey = $fallbackLanguage[fallbackLanguageIndex]; + getFallbackTranslation(langKey, translationId, interpolateParams, Interpolator).then( + deferred.resolve, + function () { + // Look in the next fallback language for a translation. + // It delays the resolving by passing another promise to resolve. + resolveForFallbackLanguage(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator, defaultTranslationText).then(deferred.resolve); + } + ); + } else { + // No translation found in any fallback language + // if a default translation text is set in the directive, then return this as a result + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + // if no default translation is set and an error handler is defined, send it to the handler + // and then return the result + deferred.resolve(translateByHandler(translationId, interpolateParams)); + } + } + return deferred.promise; + }; + + /** + * @name resolveForFallbackLanguageInstant + * @private + * + * Recursive helper function for fallbackTranslation that will sequentially look + * for a translation in the fallbackLanguages starting with fallbackLanguageIndex. + * + * @param fallbackLanguageIndex + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {string} translation + */ + var resolveForFallbackLanguageInstant = function (fallbackLanguageIndex, translationId, interpolateParams, Interpolator) { + var result; + + if (fallbackLanguageIndex < $fallbackLanguage.length) { + var langKey = $fallbackLanguage[fallbackLanguageIndex]; + result = getFallbackTranslationInstant(langKey, translationId, interpolateParams, Interpolator); + if (!result) { + result = resolveForFallbackLanguageInstant(fallbackLanguageIndex + 1, translationId, interpolateParams, Interpolator); + } + } + return result; + }; + + /** + * Translates with the usage of the fallback languages. + * + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {Q.promise} Promise, that resolves to the translation. + */ + var fallbackTranslation = function (translationId, interpolateParams, Interpolator, defaultTranslationText) { + // Start with the fallbackLanguage with index 0 + return resolveForFallbackLanguage((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator, defaultTranslationText); + }; + + /** + * Translates with the usage of the fallback languages. + * + * @param translationId + * @param interpolateParams + * @param Interpolator + * @returns {String} translation + */ + var fallbackTranslationInstant = function (translationId, interpolateParams, Interpolator) { + // Start with the fallbackLanguage with index 0 + return resolveForFallbackLanguageInstant((startFallbackIteration>0 ? startFallbackIteration : fallbackIndex), translationId, interpolateParams, Interpolator); + }; + + var determineTranslation = function (translationId, interpolateParams, interpolationId, defaultTranslationText) { + + var deferred = $q.defer(); + + var table = $uses ? $translationTable[$uses] : $translationTable, + Interpolator = (interpolationId) ? interpolatorHashMap[interpolationId] : defaultInterpolator; + + // if the translation id exists, we can just interpolate it + if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { + var translation = table[translationId]; + + // If using link, rerun $translate with linked translationId and return it + if (translation.substr(0, 2) === '@:') { + + $translate(translation.substr(2), interpolateParams, interpolationId, defaultTranslationText) + .then(deferred.resolve, deferred.reject); + } else { + deferred.resolve(Interpolator.interpolate(translation, interpolateParams)); + } + } else { + var missingTranslationHandlerTranslation; + // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise + if ($missingTranslationHandlerFactory && !pendingLoader) { + missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); + } + + // since we couldn't translate the inital requested translation id, + // we try it now with one or more fallback languages, if fallback language(s) is + // configured. + if ($uses && $fallbackLanguage && $fallbackLanguage.length) { + fallbackTranslation(translationId, interpolateParams, Interpolator, defaultTranslationText) + .then(function (translation) { + deferred.resolve(translation); + }, function (_translationId) { + deferred.reject(applyNotFoundIndicators(_translationId)); + }); + } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { + // looks like the requested translation id doesn't exists. + // Now, if there is a registered handler for missing translations and no + // asyncLoader is pending, we execute the handler + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + deferred.resolve(missingTranslationHandlerTranslation); + } + } else { + if (defaultTranslationText) { + deferred.resolve(defaultTranslationText); + } else { + deferred.reject(applyNotFoundIndicators(translationId)); + } + } + } + return deferred.promise; + }; + + var determineTranslationInstant = function (translationId, interpolateParams, interpolationId) { + + var result, table = $uses ? $translationTable[$uses] : $translationTable, + Interpolator = defaultInterpolator; + + // if the interpolation id exists use custom interpolator + if (interpolatorHashMap && Object.prototype.hasOwnProperty.call(interpolatorHashMap, interpolationId)) { + Interpolator = interpolatorHashMap[interpolationId]; + } + + // if the translation id exists, we can just interpolate it + if (table && Object.prototype.hasOwnProperty.call(table, translationId)) { + var translation = table[translationId]; + + // If using link, rerun $translate with linked translationId and return it + if (translation.substr(0, 2) === '@:') { + result = determineTranslationInstant(translation.substr(2), interpolateParams, interpolationId); + } else { + result = Interpolator.interpolate(translation, interpolateParams); + } + } else { + var missingTranslationHandlerTranslation; + // for logging purposes only (as in $translateMissingTranslationHandlerLog), value is not returned to promise + if ($missingTranslationHandlerFactory && !pendingLoader) { + missingTranslationHandlerTranslation = translateByHandler(translationId, interpolateParams); + } + + // since we couldn't translate the inital requested translation id, + // we try it now with one or more fallback languages, if fallback language(s) is + // configured. + if ($uses && $fallbackLanguage && $fallbackLanguage.length) { + fallbackIndex = 0; + result = fallbackTranslationInstant(translationId, interpolateParams, Interpolator); + } else if ($missingTranslationHandlerFactory && !pendingLoader && missingTranslationHandlerTranslation) { + // looks like the requested translation id doesn't exists. + // Now, if there is a registered handler for missing translations and no + // asyncLoader is pending, we execute the handler + result = missingTranslationHandlerTranslation; + } else { + result = applyNotFoundIndicators(translationId); + } + } + + return result; + }; + + var clearNextLangAndPromise = function(key) { + if ($nextLang === key) { + $nextLang = undefined; + } + langPromises[key] = undefined; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#preferredLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key for the preferred language. + * + * @param {string} langKey language String or Array to be used as preferredLanguage (changing at runtime) + * + * @return {string} preferred language key + */ + $translate.preferredLanguage = function (langKey) { + if(langKey) { + setupPreferredLanguage(langKey); + } + return $preferredLanguage; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#cloakClassName + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the configured class name for `translate-cloak` directive. + * + * @return {string} cloakClassName + */ + $translate.cloakClassName = function () { + return $cloakClassName; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#fallbackLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key for the fallback languages or sets a new fallback stack. + * + * @param {string=} langKey language String or Array of fallback languages to be used (to change stack at runtime) + * + * @return {string||array} fallback language key + */ + $translate.fallbackLanguage = function (langKey) { + if (langKey !== undefined && langKey !== null) { + fallbackStack(langKey); + + // as we might have an async loader initiated and a new translation language might have been defined + // we need to add the promise to the stack also. So - iterate. + if ($loaderFactory) { + if ($fallbackLanguage && $fallbackLanguage.length) { + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + if (!langPromises[$fallbackLanguage[i]]) { + langPromises[$fallbackLanguage[i]] = loadAsync($fallbackLanguage[i]); + } + } + } + } + $translate.use($translate.use()); + } + if ($fallbackWasString) { + return $fallbackLanguage[0]; + } else { + return $fallbackLanguage; + } + + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#useFallbackLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Sets the first key of the fallback language stack to be used for translation. + * Therefore all languages in the fallback array BEFORE this key will be skipped! + * + * @param {string=} langKey Contains the langKey the iteration shall start with. Set to false if you want to + * get back to the whole stack + */ + $translate.useFallbackLanguage = function (langKey) { + if (langKey !== undefined && langKey !== null) { + if (!langKey) { + startFallbackIteration = 0; + } else { + var langKeyPosition = indexOf($fallbackLanguage, langKey); + if (langKeyPosition > -1) { + startFallbackIteration = langKeyPosition; + } + } + + } + + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#proposedLanguage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the language key of language that is currently loaded asynchronously. + * + * @return {string} language key + */ + $translate.proposedLanguage = function () { + return $nextLang; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#storage + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns registered storage. + * + * @return {object} Storage + */ + $translate.storage = function () { + return Storage; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#use + * @methodOf pascalprecht.translate.$translate + * + * @description + * Tells angular-translate which language to use by given language key. This method is + * used to change language at runtime. It also takes care of storing the language + * key in a configured store to let your app remember the choosed language. + * + * When trying to 'use' a language which isn't available it tries to load it + * asynchronously with registered loaders. + * + * Returns promise object with loaded language file data + * @example + * $translate.use("en_US").then(function(data){ + * $scope.text = $translate("HELLO"); + * }); + * + * @param {string} key Language key + * @return {string} Language key + */ + $translate.use = function (key) { + if (!key) { + return $uses; + } + + var deferred = $q.defer(); + + $rootScope.$emit('$translateChangeStart', {language: key}); + + // Try to get the aliased language key + var aliasedKey = negotiateLocale(key); + if (aliasedKey) { + key = aliasedKey; + } + + // if there isn't a translation table for the language we've requested, + // we load it asynchronously + if (($forceAsyncReloadEnabled || !$translationTable[key]) && $loaderFactory && !langPromises[key]) { + $nextLang = key; + langPromises[key] = loadAsync(key).then(function (translation) { + translations(translation.key, translation.table); + deferred.resolve(translation.key); + useLanguage(translation.key); + return translation; + }, function (key) { + $rootScope.$emit('$translateChangeError', {language: key}); + deferred.reject(key); + $rootScope.$emit('$translateChangeEnd', {language: key}); + return $q.reject(key); + }); + langPromises[key]['finally'](function () { + clearNextLangAndPromise(key); + }); + } else if ($nextLang === key && langPromises[key]) { + // we are already loading this asynchronously + // resolve our new deferred when the old langPromise is resolved + langPromises[key].then(function (translation) { + deferred.resolve(translation.key); + return translation; + }, function (key) { + deferred.reject(key); + return $q.reject(key); + }); + } else { + deferred.resolve(key); + useLanguage(key); + } + + return deferred.promise; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#storageKey + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the key for the storage. + * + * @return {string} storage key + */ + $translate.storageKey = function () { + return storageKey(); + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#isPostCompilingEnabled + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether post compiling is enabled or not + * + * @return {bool} storage key + */ + $translate.isPostCompilingEnabled = function () { + return $postCompilingEnabled; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#isForceAsyncReloadEnabled + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns whether force async reload is enabled or not + * + * @return {boolean} forceAsyncReload value + */ + $translate.isForceAsyncReloadEnabled = function () { + return $forceAsyncReloadEnabled; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#refresh + * @methodOf pascalprecht.translate.$translate + * + * @description + * Refreshes a translation table pointed by the given langKey. If langKey is not specified, + * the module will drop all existent translation tables and load new version of those which + * are currently in use. + * + * Refresh means that the module will drop target translation table and try to load it again. + * + * In case there are no loaders registered the refresh() method will throw an Error. + * + * If the module is able to refresh translation tables refresh() method will broadcast + * $translateRefreshStart and $translateRefreshEnd events. + * + * @example + * // this will drop all currently existent translation tables and reload those which are + * // currently in use + * $translate.refresh(); + * // this will refresh a translation table for the en_US language + * $translate.refresh('en_US'); + * + * @param {string} langKey A language key of the table, which has to be refreshed + * + * @return {promise} Promise, which will be resolved in case a translation tables refreshing + * process is finished successfully, and reject if not. + */ + $translate.refresh = function (langKey) { + if (!$loaderFactory) { + throw new Error('Couldn\'t refresh translation table, no loader registered!'); + } + + var deferred = $q.defer(); + + function resolve() { + deferred.resolve(); + $rootScope.$emit('$translateRefreshEnd', {language: langKey}); + } + + function reject() { + deferred.reject(); + $rootScope.$emit('$translateRefreshEnd', {language: langKey}); + } + + $rootScope.$emit('$translateRefreshStart', {language: langKey}); + + if (!langKey) { + // if there's no language key specified we refresh ALL THE THINGS! + var tables = [], loadingKeys = {}; + + // reload registered fallback languages + if ($fallbackLanguage && $fallbackLanguage.length) { + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + tables.push(loadAsync($fallbackLanguage[i])); + loadingKeys[$fallbackLanguage[i]] = true; + } + } + + // reload currently used language + if ($uses && !loadingKeys[$uses]) { + tables.push(loadAsync($uses)); + } + + var allTranslationsLoaded = function (tableData) { + $translationTable = {}; + angular.forEach(tableData, function (data) { + translations(data.key, data.table); + }); + if ($uses) { + useLanguage($uses); + } + resolve(); + }; + allTranslationsLoaded.displayName = 'refreshPostProcessor'; + + $q.all(tables).then(allTranslationsLoaded, reject); + + } else if ($translationTable[langKey]) { + + var oneTranslationsLoaded = function (data) { + translations(data.key, data.table); + if (langKey === $uses) { + useLanguage($uses); + } + resolve(); + }; + oneTranslationsLoaded.displayName = 'refreshPostProcessor'; + + loadAsync(langKey).then(oneTranslationsLoaded, reject); + + } else { + reject(); + } + return deferred.promise; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#instant + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns a translation instantly from the internal state of loaded translation. All rules + * regarding the current language, the preferred language of even fallback languages will be + * used except any promise handling. If a language was not found, an asynchronous loading + * will be invoked in the background. + * + * @param {string|array} translationId A token which represents a translation id + * This can be optionally an array of translation ids which + * results that the function's promise returns an object where + * each key is the translation id and the value the translation. + * @param {object} interpolateParams Params + * @param {string} interpolationId The id of the interpolation to use + * + * @return {string|object} translation + */ + $translate.instant = function (translationId, interpolateParams, interpolationId) { + + // Detect undefined and null values to shorten the execution and prevent exceptions + if (translationId === null || angular.isUndefined(translationId)) { + return translationId; + } + + // Duck detection: If the first argument is an array, a bunch of translations was requested. + // The result is an object. + if (angular.isArray(translationId)) { + var results = {}; + for (var i = 0, c = translationId.length; i < c; i++) { + results[translationId[i]] = $translate.instant(translationId[i], interpolateParams, interpolationId); + } + return results; + } + + // We discarded unacceptable values. So we just need to verify if translationId is empty String + if (angular.isString(translationId) && translationId.length < 1) { + return translationId; + } + + // trim off any whitespace + if (translationId) { + translationId = trim.apply(translationId); + } + + var result, possibleLangKeys = []; + if ($preferredLanguage) { + possibleLangKeys.push($preferredLanguage); + } + if ($uses) { + possibleLangKeys.push($uses); + } + if ($fallbackLanguage && $fallbackLanguage.length) { + possibleLangKeys = possibleLangKeys.concat($fallbackLanguage); + } + for (var j = 0, d = possibleLangKeys.length; j < d; j++) { + var possibleLangKey = possibleLangKeys[j]; + if ($translationTable[possibleLangKey]) { + if (typeof $translationTable[possibleLangKey][translationId] !== 'undefined') { + result = determineTranslationInstant(translationId, interpolateParams, interpolationId); + } else if ($notFoundIndicatorLeft || $notFoundIndicatorRight) { + result = applyNotFoundIndicators(translationId); + } + } + if (typeof result !== 'undefined') { + break; + } + } + + if (!result && result !== '') { + // Return translation of default interpolator if not found anything. + result = defaultInterpolator.interpolate(translationId, interpolateParams); + if ($missingTranslationHandlerFactory && !pendingLoader) { + result = translateByHandler(translationId, interpolateParams); + } + } + + return result; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#versionInfo + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the current version information for the angular-translate library + * + * @return {string} angular-translate version + */ + $translate.versionInfo = function () { + return version; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translate#loaderCache + * @methodOf pascalprecht.translate.$translate + * + * @description + * Returns the defined loaderCache. + * + * @return {boolean|string|object} current value of loaderCache + */ + $translate.loaderCache = function () { + return loaderCache; + }; + + // internal purpose only + $translate.directivePriority = function () { + return directivePriority; + }; + + // internal purpose only + $translate.statefulFilter = function () { + return statefulFilter; + }; + + if ($loaderFactory) { + + // If at least one async loader is defined and there are no + // (default) translations available we should try to load them. + if (angular.equals($translationTable, {})) { + $translate.use($translate.use()); + } + + // Also, if there are any fallback language registered, we start + // loading them asynchronously as soon as we can. + if ($fallbackLanguage && $fallbackLanguage.length) { + var processAsyncResult = function (translation) { + translations(translation.key, translation.table); + $rootScope.$emit('$translateChangeEnd', { language: translation.key }); + return translation; + }; + for (var i = 0, len = $fallbackLanguage.length; i < len; i++) { + var fallbackLanguageId = $fallbackLanguage[i]; + if ($forceAsyncReloadEnabled || !$translationTable[fallbackLanguageId]) { + langPromises[fallbackLanguageId] = loadAsync(fallbackLanguageId).then(processAsyncResult); + } + } + } + } + + return $translate; + } + ]; +} +$translate.$inject = ['$STORAGE_KEY', '$windowProvider', '$translateSanitizationProvider', 'pascalprechtTranslateOverrider']; + +$translate.displayName = 'displayName'; + +/** + * @ngdoc object + * @name pascalprecht.translate.$translateDefaultInterpolation + * @requires $interpolate + * + * @description + * Uses angular's `$interpolate` services to interpolate strings against some values. + * + * Be aware to configure a proper sanitization strategy. + * + * See also: + * * {@link pascalprecht.translate.$translateSanitization} + * + * @return {object} $translateDefaultInterpolation Interpolator service + */ +angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation); + +function $translateDefaultInterpolation ($interpolate, $translateSanitization) { + + 'use strict'; + + var $translateInterpolator = {}, + $locale, + $identifier = 'default'; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#setLocale + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Sets current locale (this is currently not use in this interpolation). + * + * @param {string} locale Language key or locale. + */ + $translateInterpolator.setLocale = function (locale) { + $locale = locale; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#getInterpolationIdentifier + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Returns an identifier for this interpolation service. + * + * @returns {string} $identifier + */ + $translateInterpolator.getInterpolationIdentifier = function () { + return $identifier; + }; + + /** + * @deprecated will be removed in 3.0 + * @see {@link pascalprecht.translate.$translateSanitization} + */ + $translateInterpolator.useSanitizeValueStrategy = function (value) { + $translateSanitization.useStrategy(value); + return this; + }; + + /** + * @ngdoc function + * @name pascalprecht.translate.$translateDefaultInterpolation#interpolate + * @methodOf pascalprecht.translate.$translateDefaultInterpolation + * + * @description + * Interpolates given string agains given interpolate params using angulars + * `$interpolate` service. + * + * @returns {string} interpolated string. + */ + $translateInterpolator.interpolate = function (string, interpolationParams) { + interpolationParams = interpolationParams || {}; + interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params'); + + var interpolatedText = $interpolate(string)(interpolationParams); + interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text'); + + return interpolatedText; + }; + + return $translateInterpolator; +} +$translateDefaultInterpolation.$inject = ['$interpolate', '$translateSanitization']; + +$translateDefaultInterpolation.displayName = '$translateDefaultInterpolation'; + +angular.module('pascalprecht.translate').constant('$STORAGE_KEY', 'NG_TRANSLATE_LANG_KEY'); + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translate + * @requires $compile + * @requires $filter + * @requires $interpolate + * @restrict A + * + * @description + * Translates given translation id either through attribute or DOM content. + * Internally it uses `translate` filter to translate translation id. It possible to + * pass an optional `translate-values` object literal as string into translation id. + * + * @param {string=} translate Translation id which could be either string or interpolated string. + * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object. + * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute. + * @param {string=} translate-default will be used unless translation was successful + * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling} + * + * @example + + +
+ +

+        
TRANSLATION_ID
+

+        

+        
{{translationId}}
+

+        
WITH_VALUES
+

+        
WITH_VALUES
+

+
+      
+
+ + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider.translations('en',{ + 'TRANSLATION_ID': 'Hello there!', + 'WITH_VALUES': 'The following value is dynamic: {{value}}' + }).preferredLanguage('en'); + + }); + + angular.module('ngView').controller('TranslateCtrl', function ($scope) { + $scope.translationId = 'TRANSLATION_ID'; + + $scope.values = { + value: 78 + }; + }); + + + it('should translate', function () { + inject(function ($rootScope, $compile) { + $rootScope.translationId = 'TRANSLATION_ID'; + + element = $compile('

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

TRANSLATION_ID

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

{{translationId}}

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('Hello there!'); + + element = $compile('

')($rootScope); + $rootScope.$digest(); + expect(element.attr('title')).toBe('Hello there!'); + }); + }); +
+
+ */ +.directive('translate', translateDirective); +function translateDirective($translate, $q, $interpolate, $compile, $parse, $rootScope) { + + 'use strict'; + + /** + * @name trim + * @private + * + * @description + * trim polyfill + * + * @returns {string} The string stripped of whitespace from both ends + */ + var trim = function() { + return this.toString().replace(/^\s+|\s+$/g, ''); + }; + + return { + restrict: 'AE', + scope: true, + priority: $translate.directivePriority(), + compile: function (tElement, tAttr) { + + var translateValuesExist = (tAttr.translateValues) ? + tAttr.translateValues : undefined; + + var translateInterpolation = (tAttr.translateInterpolation) ? + tAttr.translateInterpolation : undefined; + + var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i); + + var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)', + watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)'; + + return function linkFn(scope, iElement, iAttr) { + + scope.interpolateParams = {}; + scope.preText = ''; + scope.postText = ''; + var translationIds = {}; + + var initInterpolationParams = function (interpolateParams, iAttr, tAttr) { + // initial setup + if (iAttr.translateValues) { + angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent)); + } + // initially fetch all attributes if existing and fill the params + if (translateValueExist) { + for (var attr in tAttr) { + if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { + var attributeName = angular.lowercase(attr.substr(14, 1)) + attr.substr(15); + interpolateParams[attributeName] = tAttr[attr]; + } + } + } + }; + + // Ensures any change of the attribute "translate" containing the id will + // be re-stored to the scope's "translationId". + // If the attribute has no content, the element's text value (white spaces trimmed off) will be used. + var observeElementTranslation = function (translationId) { + + // Remove any old watcher + if (angular.isFunction(observeElementTranslation._unwatchOld)) { + observeElementTranslation._unwatchOld(); + observeElementTranslation._unwatchOld = undefined; + } + + if (angular.equals(translationId , '') || !angular.isDefined(translationId)) { + // Resolve translation id by inner html if required + var interpolateMatches = trim.apply(iElement.text()).match(interpolateRegExp); + // Interpolate translation id if required + if (angular.isArray(interpolateMatches)) { + scope.preText = interpolateMatches[1]; + scope.postText = interpolateMatches[3]; + translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent); + var watcherMatches = iElement.text().match(watcherRegExp); + if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) { + observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) { + translationIds.translate = newValue; + updateTranslations(); + }); + } + } else { + translationIds.translate = iElement.text().replace(/^\s+|\s+$/g,''); + } + } else { + translationIds.translate = translationId; + } + updateTranslations(); + }; + + var observeAttributeTranslation = function (translateAttr) { + iAttr.$observe(translateAttr, function (translationId) { + translationIds[translateAttr] = translationId; + updateTranslations(); + }); + }; + + // initial setup with values + initInterpolationParams(scope.interpolateParams, iAttr, tAttr); + + var firstAttributeChangedEvent = true; + iAttr.$observe('translate', function (translationId) { + if (typeof translationId === 'undefined') { + // case of element "xyz" + observeElementTranslation(''); + } else { + // case of regular attribute + if (translationId !== '' || !firstAttributeChangedEvent) { + translationIds.translate = translationId; + updateTranslations(); + } + } + firstAttributeChangedEvent = false; + }); + + for (var translateAttr in iAttr) { + if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr') { + observeAttributeTranslation(translateAttr); + } + } + + iAttr.$observe('translateDefault', function (value) { + scope.defaultText = value; + }); + + if (translateValuesExist) { + iAttr.$observe('translateValues', function (interpolateParams) { + if (interpolateParams) { + scope.$parent.$watch(function () { + angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent)); + }); + } + }); + } + + if (translateValueExist) { + var observeValueAttribute = function (attrName) { + iAttr.$observe(attrName, function (value) { + var attributeName = angular.lowercase(attrName.substr(14, 1)) + attrName.substr(15); + scope.interpolateParams[attributeName] = value; + }); + }; + for (var attr in iAttr) { + if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { + observeValueAttribute(attr); + } + } + } + + // Master update function + var updateTranslations = function () { + for (var key in translationIds) { + + if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) { + updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText); + } + } + }; + + // Put translation processing function outside loop + var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText) { + if (translationId) { + $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText) + .then(function (translation) { + applyTranslation(translation, scope, true, translateAttr); + }, function (translationId) { + applyTranslation(translationId, scope, false, translateAttr); + }); + } else { + // as an empty string cannot be translated, we can solve this using successful=false + applyTranslation(translationId, scope, false, translateAttr); + } + }; + + var applyTranslation = function (value, scope, successful, translateAttr) { + if (translateAttr === 'translate') { + // default translate into innerHTML + if (!successful && typeof scope.defaultText !== 'undefined') { + value = scope.defaultText; + } + iElement.html(scope.preText + value + scope.postText); + var globallyEnabled = $translate.isPostCompilingEnabled(); + var locallyDefined = typeof tAttr.translateCompile !== 'undefined'; + var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false'; + if ((globallyEnabled && !locallyDefined) || locallyEnabled) { + $compile(iElement.contents())(scope); + } + } else { + // translate attribute + if (!successful && typeof scope.defaultText !== 'undefined') { + value = scope.defaultText; + } + var attributeName = iAttr.$attr[translateAttr]; + if (attributeName.substr(0, 5) === 'data-') { + // ensure html5 data prefix is stripped + attributeName = attributeName.substr(5); + } + attributeName = attributeName.substr(15); + iElement.attr(attributeName, value); + } + }; + + if (translateValuesExist || translateValueExist || iAttr.translateDefault) { + scope.$watch('interpolateParams', updateTranslations, true); + } + + // Ensures the text will be refreshed after the current language was changed + // w/ $translate.use(...) + var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations); + + // ensure translation will be looked up at least one + if (iElement.text().length) { + if (iAttr.translate) { + observeElementTranslation(iAttr.translate); + } else { + observeElementTranslation(''); + } + } else if (iAttr.translate) { + // ensure attribute will be not skipped + observeElementTranslation(iAttr.translate); + } + updateTranslations(); + scope.$on('$destroy', unbind); + }; + } + }; +} +translateDirective.$inject = ['$translate', '$q', '$interpolate', '$compile', '$parse', '$rootScope']; + +translateDirective.displayName = 'translateDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc directive + * @name pascalprecht.translate.directive:translateCloak + * @requires $rootScope + * @requires $translate + * @restrict A + * + * $description + * Adds a `translate-cloak` class name to the given element where this directive + * is applied initially and removes it, once a loader has finished loading. + * + * This directive can be used to prevent initial flickering when loading translation + * data asynchronously. + * + * The class name is defined in + * {@link pascalprecht.translate.$translateProvider#cloakClassName $translate.cloakClassName()}. + * + * @param {string=} translate-cloak If a translationId is provided, it will be used for showing + * or hiding the cloak. Basically it relies on the translation + * resolve. + */ +.directive('translateCloak', translateCloakDirective); + +function translateCloakDirective($rootScope, $translate) { + + 'use strict'; + + return { + compile: function (tElement) { + var applyCloak = function () { + tElement.addClass($translate.cloakClassName()); + }, + removeCloak = function () { + tElement.removeClass($translate.cloakClassName()); + }, + removeListener = $rootScope.$on('$translateChangeEnd', function () { + removeCloak(); + removeListener(); + removeListener = null; + }); + applyCloak(); + + return function linkFn(scope, iElement, iAttr) { + // Register a watcher for the defined translation allowing a fine tuned cloak + if (iAttr.translateCloak && iAttr.translateCloak.length) { + iAttr.$observe('translateCloak', function (translationId) { + $translate(translationId).then(removeCloak, applyCloak); + }); + } + }; + } + }; +} +translateCloakDirective.$inject = ['$rootScope', '$translate']; + +translateCloakDirective.displayName = 'translateCloakDirective'; + +angular.module('pascalprecht.translate') +/** + * @ngdoc filter + * @name pascalprecht.translate.filter:translate + * @requires $parse + * @requires pascalprecht.translate.$translate + * @function + * + * @description + * Uses `$translate` service to translate contents. Accepts interpolate parameters + * to pass dynamized values though translation. + * + * @param {string} translationId A translation id to be translated. + * @param {*=} interpolateParams Optional object literal (as hash or string) to pass values into translation. + * + * @returns {string} Translated text. + * + * @example + + +
+ +
{{ 'TRANSLATION_ID' | translate }}
+
{{ translationId | translate }}
+
{{ 'WITH_VALUES' | translate:'{value: 5}' }}
+
{{ 'WITH_VALUES' | translate:values }}
+ +
+
+ + angular.module('ngView', ['pascalprecht.translate']) + + .config(function ($translateProvider) { + + $translateProvider.translations('en', { + 'TRANSLATION_ID': 'Hello there!', + 'WITH_VALUES': 'The following value is dynamic: {{value}}' + }); + $translateProvider.preferredLanguage('en'); + + }); + + angular.module('ngView').controller('TranslateCtrl', function ($scope) { + $scope.translationId = 'TRANSLATION_ID'; + + $scope.values = { + value: 78 + }; + }); + +
+ */ +.filter('translate', translateFilterFactory); + +function translateFilterFactory($parse, $translate) { + + 'use strict'; + + var translateFilter = function (translationId, interpolateParams, interpolation) { + + if (!angular.isObject(interpolateParams)) { + interpolateParams = $parse(interpolateParams)(this); + } + + return $translate.instant(translationId, interpolateParams, interpolation); + }; + + if ($translate.statefulFilter()) { + translateFilter.$stateful = true; + } + + return translateFilter; +} +translateFilterFactory.$inject = ['$parse', '$translate']; + +translateFilterFactory.displayName = 'translateFilterFactory'; + +angular.module('pascalprecht.translate') + +/** + * @ngdoc object + * @name pascalprecht.translate.$translationCache + * @requires $cacheFactory + * + * @description + * The first time a translation table is used, it is loaded in the translation cache for quick retrieval. You + * can load translation tables directly into the cache by consuming the + * `$translationCache` service directly. + * + * @return {object} $cacheFactory object. + */ + .factory('$translationCache', $translationCache); + +function $translationCache($cacheFactory) { + + 'use strict'; + + return $cacheFactory('translations'); +} +$translationCache.$inject = ['$cacheFactory']; + +$translationCache.displayName = '$translationCache'; +return 'pascalprecht.translate'; + +})); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 1de1c7e381..1a16fbcd26 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.login.freemarker; import org.jboss.logging.Logger; @@ -241,7 +257,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } if (client != null) { - attributes.put("client", new ClientBean(client)); + attributes.put("client", new ClientBean(client, baseUri)); } attributes.put("login", new LoginBean(formData)); @@ -276,7 +292,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); } - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName())); + + String cookiePath = Urls.localeCookiePath(baseUri, realm.getName()); + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath); + return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -322,7 +341,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { logger.warn("Failed to load properties", e); } if (client != null) { - attributes.put("client", new ClientBean(client)); + attributes.put("client", new ClientBean(client, baseUri)); } Properties messagesBundle; @@ -374,7 +393,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); } - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName())); + + String cookiePath = Urls.localeCookiePath(baseUri, realm.getName()); + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -383,26 +404,32 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } + @Override public Response createLogin() { return createResponse(LoginFormsPages.LOGIN); } + @Override public Response createPasswordReset() { return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD); } + @Override public Response createLoginTotp() { return createResponse(LoginFormsPages.LOGIN_TOTP); } + @Override public Response createRegistration() { return createResponse(LoginFormsPages.REGISTER); } + @Override public Response createInfoPage() { return createResponse(LoginFormsPages.INFO); } + @Override public Response createErrorPage() { if (status == null) { status = Response.Status.INTERNAL_SERVER_ERROR; @@ -410,7 +437,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.ERROR); } - + @Override public Response createOAuthGrant(ClientSessionModel clientSession) { this.clientSession = clientSession; return createResponse(LoginFormsPages.OAUTH_GRANT); @@ -494,11 +521,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override public FreeMarkerLoginFormsProvider setUser(UserModel user) { this.user = user; return this; } + @Override public FreeMarkerLoginFormsProvider setFormData(MultivaluedMap formData) { this.formData = formData; return this; diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java index 0404ea78c2..f10d031d07 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ClientBean.java @@ -1,6 +1,9 @@ package org.keycloak.login.freemarker.model; import org.keycloak.models.ClientModel; +import org.keycloak.services.util.ResolveRelative; + +import java.net.URI; /** * @author Bill Burke @@ -10,8 +13,11 @@ public class ClientBean { protected ClientModel client; - public ClientBean(ClientModel client) { + private URI requestUri; + + public ClientBean(ClientModel client, URI requestUri) { this.client = client; + this.requestUri = requestUri; } public String getClientId() { @@ -23,7 +29,7 @@ public class ClientBean { } public String getBaseUrl() { - return client.getBaseUrl(); + return ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), client.getBaseUrl()); } } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java index ad5315b900..87513ec2ab 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -1,13 +1,9 @@ package org.keycloak.admin.client.resource; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import java.util.List; import java.util.Map; @@ -28,6 +24,12 @@ public interface RealmResource { @Path("clients") ClientsResource clients(); + @Path("client-description-converter") + @POST + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) + @Produces(MediaType.APPLICATION_JSON) + ClientRepresentation convertClientDescription(String description); + @Path("users") UsersResource users(); diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java index daccf8e0aa..ee4317cf5c 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java @@ -56,6 +56,10 @@ public interface ClientModel extends RoleContainerModel { void setManagementUrl(String url); + String getRootUrl(); + + void setRootUrl(String url); + String getBaseUrl(); void setBaseUrl(String url); diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java index 52e972196b..4c742abc71 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java @@ -24,6 +24,7 @@ public class ClientEntity extends AbstractIdentifiableEntity { private boolean surrogateAuthRequired; private String managementUrl; + private String rootUrl; private String baseUrl; private boolean bearerOnly; private boolean consentRequired; @@ -196,6 +197,14 @@ public class ClientEntity extends AbstractIdentifiableEntity { this.managementUrl = managementUrl; } + public String getRootUrl() { + return rootUrl; + } + + public void setRootUrl(String rootUrl) { + this.rootUrl = rootUrl; + } + public String getBaseUrl() { return baseUrl; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index b71fce9ff4..9906ab2466 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -306,6 +306,7 @@ public class ModelToRepresentation { rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled()); rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly()); rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired()); + rep.setRootUrl(clientModel.getRootUrl()); rep.setBaseUrl(clientModel.getBaseUrl()); rep.setNotBefore(clientModel.getNotBefore()); rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 8a79f01ba9..ffb9673658 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -692,6 +692,7 @@ public class RepresentationToModel { client.setManagementUrl(resourceRep.getAdminUrl()); if (resourceRep.isSurrogateAuthRequired() != null) client.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired()); + if (resourceRep.getRootUrl() != null) client.setRootUrl(resourceRep.getRootUrl()); if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl()); if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired()); @@ -796,6 +797,7 @@ public class RepresentationToModel { if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient()); if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed()); if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout()); + if (rep.getRootUrl() != null) resource.setRootUrl(rep.getRootUrl()); if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl()); if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl()); if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired()); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java index 8003b70ded..1ddc36499b 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java @@ -421,6 +421,16 @@ public class ClientAdapter implements ClientModel { entity.setManagementUrl(url); } + @Override + public void setRootUrl(String url) { + entity.setRootUrl(url); + } + + @Override + public String getRootUrl() { + return entity.getRootUrl(); + } + @Override public void setBaseUrl(String url) { entity.setBaseUrl(url); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 9f79b157ed..e1819787ce 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -345,6 +345,18 @@ public class ClientAdapter implements ClientModel { updated.setManagementUrl(url); } + @Override + public String getRootUrl() { + if (updated != null) return updated.getRootUrl(); + return cached.getRootUrl(); + } + + @Override + public void setRootUrl(String url) { + getDelegateForUpdate(); + updated.setRootUrl(url); + } + @Override public String getBaseUrl() { if (updated != null) return updated.getBaseUrl(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java index 11447d02d7..3015acfba6 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java @@ -42,6 +42,7 @@ public class CachedClient implements Serializable { private Set protocolMappers = new HashSet(); private boolean surrogateAuthRequired; private String managementUrl; + private String rootUrl; private String baseUrl; private List defaultRoles = new LinkedList(); private boolean bearerOnly; @@ -76,6 +77,7 @@ public class CachedClient implements Serializable { } surrogateAuthRequired = model.isSurrogateAuthRequired(); managementUrl = model.getManagementUrl(); + rootUrl = model.getRootUrl(); baseUrl = model.getBaseUrl(); defaultRoles.addAll(model.getDefaultRoles()); bearerOnly = model.isBearerOnly(); @@ -169,6 +171,10 @@ public class CachedClient implements Serializable { return managementUrl; } + public String getRootUrl() { + return rootUrl; + } + public String getBaseUrl() { return baseUrl; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index b0acc417fd..1e0c21edaa 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -441,6 +441,16 @@ public class ClientAdapter implements ClientModel { entity.setManagementUrl(url); } + @Override + public String getRootUrl() { + return entity.getRootUrl(); + } + + @Override + public void setRootUrl(String url) { + entity.setRootUrl(url); + } + @Override public String getBaseUrl() { return entity.getBaseUrl(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java index 1f6ac251b3..b0a30cd67f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java @@ -82,6 +82,9 @@ public class ClientEntity { @Column(name="SURROGATE_AUTH_REQUIRED") private boolean surrogateAuthRequired; + @Column(name="ROOT_URL") + private String rootUrl; + @Column(name="BASE_URL") private String baseUrl; @@ -260,6 +263,14 @@ public class ClientEntity { this.surrogateAuthRequired = surrogateAuthRequired; } + public String getRootUrl() { + return rootUrl; + } + + public void setRootUrl(String rootUrl) { + this.rootUrl = rootUrl; + } + public String getBaseUrl() { return baseUrl; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index 26effca9c3..4e1b4faf23 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -439,6 +439,17 @@ public class ClientAdapter extends AbstractMongoAdapter imple updateMongoEntity(); } + @Override + public void setRootUrl(String url) { + getMongoEntity().setRootUrl(url); + updateMongoEntity(); + } + + @Override + public String getRootUrl() { + return getMongoEntity().getRootUrl(); + } + @Override public void setBaseUrl(String url) { getMongoEntity().setBaseUrl(url); diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java new file mode 100755 index 0000000000..da9961301a --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorDescriptionConverter.java @@ -0,0 +1,167 @@ +package org.keycloak.protocol.saml; + +import org.keycloak.Config; +import org.keycloak.dom.saml.v2.metadata.*; +import org.keycloak.exportimport.ClientDescriptionConverter; +import org.keycloak.exportimport.ClientDescriptionConverterFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.saml.SignatureAlgorithm; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.common.exceptions.ConfigurationException; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.saml.processing.core.saml.v2.util.SAMLMetadataUtil; +import org.keycloak.saml.processing.core.util.CoreConfigUtil; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class EntityDescriptorDescriptionConverter implements ClientDescriptionConverter, ClientDescriptionConverterFactory { + + @Override + public boolean isSupported(String description) { + description = description.trim(); + return (description.startsWith("<") && description.endsWith(">") && description.contains("EntityDescriptor")); + } + + @Override + public ClientRepresentation convertToInternal(String description) { + return loadEntityDescriptors(new ByteArrayInputStream(description.getBytes())); + } + + private static ClientRepresentation loadEntityDescriptors(InputStream is) { + Object metadata; + try { + metadata = new SAMLParser().parse(is); + } catch (ParsingException e) { + throw new RuntimeException(e); + } + EntitiesDescriptorType entities; + + if (EntitiesDescriptorType.class.isInstance(metadata)) { + entities = (EntitiesDescriptorType) metadata; + } else { + entities = new EntitiesDescriptorType(); + entities.addEntityDescriptor(metadata); + } + + if (entities.getEntityDescriptor().size() != 1) { + throw new RuntimeException("Expected one entity descriptor"); + } + + EntityDescriptorType entity = (EntityDescriptorType) entities.getEntityDescriptor().get(0); + String entityId = entity.getEntityID(); + + ClientRepresentation app = new ClientRepresentation(); + app.setClientId(entityId); + + Map attributes = new HashMap<>(); + app.setAttributes(attributes); + + List redirectUris = new LinkedList<>(); + app.setRedirectUris(redirectUris); + + app.setFullScopeAllowed(true); + app.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + attributes.put(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true + attributes.put(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString()); + attributes.put(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity); + if (spDescriptorType.isWantAssertionsSigned()) { + attributes.put(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + } + String logoutPost = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); + if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost); + String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); + if (logoutPost != null) attributes.put(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect); + + String assertionConsumerServicePostBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); + if (assertionConsumerServicePostBinding != null) { + attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, assertionConsumerServicePostBinding); + redirectUris.add(assertionConsumerServicePostBinding); + } + String assertionConsumerServiceRedirectBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); + if (assertionConsumerServiceRedirectBinding != null) { + attributes.put(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding); + redirectUris.add(assertionConsumerServiceRedirectBinding); + } + + for (KeyDescriptorType keyDescriptor : spDescriptorType.getKeyDescriptor()) { + X509Certificate cert = null; + try { + cert = SAMLMetadataUtil.getCertificate(keyDescriptor); + } catch (ConfigurationException e) { + throw new RuntimeException(e); + } catch (ProcessingException e) { + throw new RuntimeException(e); + } + String certPem = KeycloakModelUtils.getPemFromCertificate(cert); + if (keyDescriptor.getUse() == KeyTypes.SIGNING) { + attributes.put(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem); + } else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) { + attributes.put(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); + attributes.put(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem); + } + } + + return app; + } + + private static String getLogoutLocation(SPSSODescriptorType idp, String bindingURI) { + String logoutResponseLocation = null; + + List endpoints = idp.getSingleLogoutService(); + for (EndpointType endpoint : endpoints) { + if (endpoint.getBinding().toString().equals(bindingURI)) { + if (endpoint.getLocation() != null) { + logoutResponseLocation = endpoint.getLocation().toString(); + } else { + logoutResponseLocation = null; + } + + break; + } + + } + return logoutResponseLocation; + } + + @Override + public ClientDescriptionConverter create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "saml2-entity-descriptor"; + } + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java deleted file mode 100755 index a46614a96f..0000000000 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporter.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.keycloak.protocol.saml; - -import org.keycloak.exportimport.ClientImporter; -import org.keycloak.models.RealmModel; -import org.keycloak.services.resources.admin.RealmAuth; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class EntityDescriptorImporter implements ClientImporter { - @Override - public Object createJaxrsService(RealmModel realm, RealmAuth auth) { - return new EntityDescriptorImporterService(realm, auth); - } - - @Override - public void close() { - - } -} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java deleted file mode 100755 index ccd78905aa..0000000000 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterFactory.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.keycloak.protocol.saml; - -import org.keycloak.Config; -import org.keycloak.exportimport.ClientImporter; -import org.keycloak.exportimport.ClientImporterFactory; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class EntityDescriptorImporterFactory implements ClientImporterFactory { - @Override - public String getDisplayName() { - return "SAML 2.0 Entity Descriptor"; - } - - @Override - public ClientImporter create(KeycloakSession session) { - return new EntityDescriptorImporter(); - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - @Override - public void close() { - - } - - @Override - public String getId() { - return "saml2-entity-descriptor"; - } -} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java deleted file mode 100755 index 8cdd302faf..0000000000 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/EntityDescriptorImporterService.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.keycloak.protocol.saml; - -import org.jboss.resteasy.plugins.providers.multipart.InputPart; -import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; -import org.keycloak.models.ClientModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.saml.SignatureAlgorithm; -import org.keycloak.services.resources.admin.RealmAuth; -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; -import org.keycloak.saml.common.exceptions.ConfigurationException; -import org.keycloak.saml.common.exceptions.ParsingException; -import org.keycloak.saml.common.exceptions.ProcessingException; -import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; -import org.keycloak.saml.processing.core.saml.v2.util.SAMLMetadataUtil; -import org.keycloak.saml.processing.core.util.CoreConfigUtil; -import org.keycloak.dom.saml.v2.metadata.EndpointType; -import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType; -import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType; -import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType; -import org.keycloak.dom.saml.v2.metadata.KeyTypes; -import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; - -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.io.InputStream; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Map; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class EntityDescriptorImporterService { - protected RealmModel realm; - protected RealmAuth auth; - - public EntityDescriptorImporterService(RealmModel realm, RealmAuth auth) { - this.realm = realm; - this.auth = auth; - } - - @POST - @Path("upload") - @Consumes(MediaType.MULTIPART_FORM_DATA) - public void updateEntityDescriptor(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException { - auth.requireManage(); - - Map> uploadForm = input.getFormDataMap(); - List inputParts = uploadForm.get("file"); - - InputStream is = inputParts.get(0).getBody(InputStream.class, null); - - loadEntityDescriptors(is, realm); - - } - - public static void loadEntityDescriptors(InputStream is, RealmModel realm) { - Object metadata = null; - try { - metadata = new SAMLParser().parse(is); - } catch (ParsingException e) { - throw new RuntimeException(e); - } - EntitiesDescriptorType entities; - - if (EntitiesDescriptorType.class.isInstance(metadata)) { - entities = (EntitiesDescriptorType) metadata; - } else { - entities = new EntitiesDescriptorType(); - entities.addEntityDescriptor(metadata); - } - - for (Object o : entities.getEntityDescriptor()) { - EntityDescriptorType entity = (EntityDescriptorType)o; - String entityId = entity.getEntityID(); - ClientModel app = realm.addClient(entityId); - app.setFullScopeAllowed(true); - app.setProtocol(SamlProtocol.LOGIN_PROTOCOL); - app.setAttribute(SamlProtocol.SAML_SERVER_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); // default to true - app.setAttribute(SamlProtocol.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.RSA_SHA256.toString()); - app.setAttribute(SamlProtocol.SAML_AUTHNSTATEMENT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - SPSSODescriptorType spDescriptorType = CoreConfigUtil.getSPDescriptor(entity); - if (spDescriptorType.isWantAssertionsSigned()) { - app.setAttribute(SamlProtocol.SAML_ASSERTION_SIGNATURE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - } - String logoutPost = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - if (logoutPost != null) app.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, logoutPost); - String logoutRedirect = getLogoutLocation(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - if (logoutPost != null) app.setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, logoutRedirect); - - String assertionConsumerServicePostBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get()); - if (assertionConsumerServicePostBinding != null) { - app.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE, assertionConsumerServicePostBinding); - app.addRedirectUri(assertionConsumerServicePostBinding); - } - String assertionConsumerServiceRedirectBinding = CoreConfigUtil.getServiceURL(spDescriptorType, JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get()); - if (assertionConsumerServiceRedirectBinding != null) { - app.setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE, assertionConsumerServiceRedirectBinding); - app.addRedirectUri(assertionConsumerServiceRedirectBinding); - } - - for (KeyDescriptorType keyDescriptor : spDescriptorType.getKeyDescriptor()) { - X509Certificate cert = null; - try { - cert = SAMLMetadataUtil.getCertificate(keyDescriptor); - } catch (ConfigurationException e) { - throw new RuntimeException(e); - } catch (ProcessingException e) { - throw new RuntimeException(e); - } - String certPem = KeycloakModelUtils.getPemFromCertificate(cert); - if (keyDescriptor.getUse() == KeyTypes.SIGNING) { - app.setAttribute(SamlProtocol.SAML_CLIENT_SIGNATURE_ATTRIBUTE, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - app.setAttribute(SamlProtocol.SAML_SIGNING_CERTIFICATE_ATTRIBUTE, certPem); - } else if (keyDescriptor.getUse() == KeyTypes.ENCRYPTION) { - app.setAttribute(SamlProtocol.SAML_ENCRYPT, SamlProtocol.ATTRIBUTE_TRUE_VALUE); - app.setAttribute(SamlProtocol.SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE, certPem); - } - } - } - } - - public static String getLogoutLocation(SPSSODescriptorType idp, String bindingURI) { - String logoutResponseLocation = null; - - List endpoints = idp.getSingleLogoutService(); - for (EndpointType endpoint : endpoints) { - if (endpoint.getBinding().toString().equals(bindingURI)) { - if (endpoint.getLocation() != null) { - logoutResponseLocation = endpoint.getLocation().toString(); - } else { - logoutResponseLocation = null; - } - - break; - } - - } - return logoutResponseLocation; - } - - -} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index d6b745bb43..8d6fa15823 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -485,7 +485,7 @@ public class SamlProtocol implements LoginProtocol { } if (logoutServiceUrl == null && client instanceof ClientModel) logoutServiceUrl = ((ClientModel)client).getManagementUrl(); if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null; - return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), logoutServiceUrl); + return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl); } diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory new file mode 100755 index 0000000000..b874ead696 --- /dev/null +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory @@ -0,0 +1 @@ +org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter \ No newline at end of file diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientImporterFactory b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientImporterFactory deleted file mode 100755 index 0971c24781..0000000000 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientImporterFactory +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.protocol.saml.EntityDescriptorImporterFactory \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/exportimport/ClientImporter.java b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverter.java similarity index 55% rename from services/src/main/java/org/keycloak/exportimport/ClientImporter.java rename to services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverter.java index e4f78210fe..debab4de96 100755 --- a/services/src/main/java/org/keycloak/exportimport/ClientImporter.java +++ b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverter.java @@ -1,8 +1,7 @@ package org.keycloak.exportimport; -import org.keycloak.models.RealmModel; import org.keycloak.provider.Provider; -import org.keycloak.services.resources.admin.RealmAuth; +import org.keycloak.representations.idm.ClientRepresentation; /** * Provider plugin interface for importing clients from an arbitrary configuration format @@ -10,6 +9,8 @@ import org.keycloak.services.resources.admin.RealmAuth; * @author Bill Burke * @version $Revision: 1 $ */ -public interface ClientImporter extends Provider { - public Object createJaxrsService(RealmModel realm, RealmAuth auth); +public interface ClientDescriptionConverter extends Provider { + + ClientRepresentation convertToInternal(String description); + } diff --git a/services/src/main/java/org/keycloak/exportimport/ClientImporterFactory.java b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterFactory.java similarity index 64% rename from services/src/main/java/org/keycloak/exportimport/ClientImporterFactory.java rename to services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterFactory.java index 4cb47cdb6f..5609b76322 100755 --- a/services/src/main/java/org/keycloak/exportimport/ClientImporterFactory.java +++ b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterFactory.java @@ -8,6 +8,8 @@ import org.keycloak.provider.ProviderFactory; * @author Bill Burke * @version $Revision: 1 $ */ -public interface ClientImporterFactory extends ProviderFactory { - public String getDisplayName(); +public interface ClientDescriptionConverterFactory extends ProviderFactory { + + boolean isSupported(String description); + } diff --git a/services/src/main/java/org/keycloak/exportimport/ClientImportSpi.java b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterSpi.java similarity index 71% rename from services/src/main/java/org/keycloak/exportimport/ClientImportSpi.java rename to services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterSpi.java index 47a777e307..d154e79158 100755 --- a/services/src/main/java/org/keycloak/exportimport/ClientImportSpi.java +++ b/services/src/main/java/org/keycloak/exportimport/ClientDescriptionConverterSpi.java @@ -7,7 +7,7 @@ import org.keycloak.provider.Spi; /** * @author Marek Posolda */ -public class ClientImportSpi implements Spi { +public class ClientDescriptionConverterSpi implements Spi { @Override public boolean isInternal() { @@ -16,16 +16,17 @@ public class ClientImportSpi implements Spi { @Override public String getName() { - return "client-import"; + return "client-description-converter"; } @Override public Class getProviderClass() { - return ClientImporter.class; + return ClientDescriptionConverter.class; } @Override public Class getProviderFactoryClass() { - return ClientImporterFactory.class; + return ClientDescriptionConverterFactory.class; } + } diff --git a/services/src/main/java/org/keycloak/exportimport/KeycloakClientDescriptionConverter.java b/services/src/main/java/org/keycloak/exportimport/KeycloakClientDescriptionConverter.java new file mode 100644 index 0000000000..c793fe0b29 --- /dev/null +++ b/services/src/main/java/org/keycloak/exportimport/KeycloakClientDescriptionConverter.java @@ -0,0 +1,58 @@ +package org.keycloak.exportimport; + +import org.keycloak.Config; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; + +/** + * @author Stian Thorgersen + */ +public class KeycloakClientDescriptionConverter implements ClientDescriptionConverterFactory, ClientDescriptionConverter { + + public static final String ID = "keycloak"; + + @Override + public boolean isSupported(String description) { + description = description.trim(); + return (description.startsWith("{") && description.endsWith("}") && description.contains("\"clientId\"")); + } + + @Override + public ClientRepresentation convertToInternal(String description) { + try { + return JsonSerialization.readValue(description, ClientRepresentation.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public ClientDescriptionConverter create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java new file mode 100644 index 0000000000..56b47bb585 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCClientDescriptionConverter.java @@ -0,0 +1,65 @@ +package org.keycloak.protocol.oidc; + +import org.keycloak.Config; +import org.keycloak.exportimport.ClientDescriptionConverter; +import org.keycloak.exportimport.ClientDescriptionConverterFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.representations.OIDCClientRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; + +/** + * @author Stian Thorgersen + */ +public class OIDCClientDescriptionConverter implements ClientDescriptionConverter, ClientDescriptionConverterFactory { + + @Override + public boolean isSupported(String description) { + description = description.trim(); + return (description.startsWith("{") && description.endsWith("}") && description.contains("\"redirect_uris\"")); + } + + @Override + public ClientRepresentation convertToInternal(String description) { + try { + OIDCClientRepresentation oidcRep = JsonSerialization.readValue(description, OIDCClientRepresentation.class); + + ClientRepresentation client = new ClientRepresentation(); + client.setClientId(KeycloakModelUtils.generateId()); + client.setName(oidcRep.getClientName()); + client.setRedirectUris(oidcRep.getRedirectUris()); + client.setBaseUrl(oidcRep.getClientUri()); + + return client; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public ClientDescriptionConverter create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "openid-connect"; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index 8f62a52344..c600322719 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.protocol.oidc; import org.keycloak.constants.KerberosConstants; @@ -19,6 +35,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; /** * @author Bill Burke @@ -32,12 +49,14 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { public static final String GIVEN_NAME = "given name"; public static final String FAMILY_NAME = "family name"; public static final String FULL_NAME = "full name"; + public static final String LOCALE = "locale"; public static final String USERNAME_CONSENT_TEXT = "${username}"; public static final String EMAIL_CONSENT_TEXT = "${email}"; public static final String EMAIL_VERIFIED_CONSENT_TEXT = "${emailVerified}"; public static final String GIVEN_NAME_CONSENT_TEXT = "${givenName}"; public static final String FAMILY_NAME_CONSENT_TEXT = "${familyName}"; public static final String FULL_NAME_CONSENT_TEXT = "${fullName}"; + public static final String LOCALE_CONSENT_TEXT = "${locale}"; @Override @@ -95,6 +114,12 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { false, EMAIL_VERIFIED_CONSENT_TEXT, true, true); builtins.add(model); + model = UserAttributeMapper.createClaimMapper(LOCALE, + "locale", + "locale", "String", + false, LOCALE_CONSENT_TEXT, + true, true, false); + builtins.add(model); ProtocolMapperModel fullName = new ProtocolMapperModel(); fullName.setName(FULL_NAME); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index c43140a1bf..c166565ddf 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -24,6 +24,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; +import org.keycloak.protocol.oidc.utils.WebOriginsUtils; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -217,7 +218,7 @@ public class TokenManager { } public AccessToken createClientAccessToken(KeycloakSession session, Set requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) { - AccessToken token = initToken(realm, client, user, userSession, clientSession); + AccessToken token = initToken(realm, client, user, userSession, clientSession, session.getContext().getUri()); for (RoleModel role : requestedRoles) { addComposites(token, role); } @@ -380,7 +381,7 @@ public class TokenManager { } - protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession) { + protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession, UriInfo uriInfo) { AccessToken token = new AccessToken(); if (clientSession != null) token.clientSession(clientSession.getId()); token.id(KeycloakModelUtils.generateId()); @@ -398,7 +399,7 @@ public class TokenManager { } Set allowedOrigins = client.getWebOrigins(); if (allowedOrigins != null) { - token.setAllowedOrigins(allowedOrigins); + token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client)); } return token; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java index 156ee4465c..17c3bdbda6 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -58,7 +58,7 @@ public class LoginStatusIframeEndpoint { } } - for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRedirectUris())) { + for (String r : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) { int i = r.indexOf('/', 8); if (i != -1) { r = r.substring(0, i); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java new file mode 100644 index 0000000000..7de415c984 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCClientRepresentation.java @@ -0,0 +1,45 @@ +package org.keycloak.protocol.oidc.representations; + +import org.codehaus.jackson.annotate.JsonProperty; + +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class OIDCClientRepresentation { + + @JsonProperty("redirect_uris") + private List redirectUris; + + @JsonProperty("client_name") + private String clientName; + + @JsonProperty("client_uri") + private String clientUri; + + public List getRedirectUris() { + return redirectUris; + } + + public void setRedirectUris(List redirectUris) { + this.redirectUris = redirectUris; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getClientUri() { + return clientUri; + } + + public void setClientUri(String clientUri) { + this.clientUri = clientUri; + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java index df650f6c79..ff4601f1a4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -19,22 +19,22 @@ public class RedirectUtils { private static final Logger logger = Logger.getLogger(RedirectUtils.class); public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) { - Set validRedirects = getValidateRedirectUris(realm); - return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); + Set validRedirects = getValidateRedirectUris(uriInfo, realm); + return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects); } public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) { Set validRedirects = client.getRedirectUris(); - return verifyRedirectUri(uriInfo, redirectUri, realm, validRedirects); + return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, validRedirects); } - public static Set resolveValidRedirects(UriInfo uriInfo, Set validRedirects) { + public static Set resolveValidRedirects(UriInfo uriInfo, String rootUrl, Set validRedirects) { // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port Set resolveValidRedirects = new HashSet(); for (String validRedirect : validRedirects) { resolveValidRedirects.add(validRedirect); // add even relative urls. if (validRedirect.startsWith("/")) { - validRedirect = relativeToAbsoluteURI(uriInfo, validRedirect); + validRedirect = relativeToAbsoluteURI(uriInfo, rootUrl, validRedirect); logger.debugv("replacing relative valid redirect with: {0}", validRedirect); resolveValidRedirects.add(validRedirect); } @@ -42,17 +42,15 @@ public class RedirectUtils { return resolveValidRedirects; } - private static Set getValidateRedirectUris(RealmModel realm) { - Set redirects = new HashSet(); + private static Set getValidateRedirectUris(UriInfo uriInfo, RealmModel realm) { + Set redirects = new HashSet<>(); for (ClientModel client : realm.getClients()) { - for (String redirect : client.getRedirectUris()) { - redirects.add(redirect); - } + redirects.addAll(resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())); } return redirects; } - private static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, Set validRedirects) { + private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set validRedirects) { if (redirectUri == null) { if (validRedirects.size() != 1) return null; String validRedirect = validRedirects.iterator().next(); @@ -66,7 +64,7 @@ public class RedirectUtils { redirectUri = null; } else { String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; - Set resolveValidRedirects = resolveValidRedirects(uriInfo, validRedirects); + Set resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects); boolean valid = matchesRedirects(resolveValidRedirects, r); @@ -86,7 +84,7 @@ public class RedirectUtils { valid = matchesRedirects(resolveValidRedirects, r); } if (valid && redirectUri.startsWith("/")) { - redirectUri = relativeToAbsoluteURI(uriInfo, redirectUri); + redirectUri = relativeToAbsoluteURI(uriInfo, rootUrl, redirectUri); } redirectUri = valid ? redirectUri : null; } @@ -98,13 +96,16 @@ public class RedirectUtils { } } - private static String relativeToAbsoluteURI(UriInfo uriInfo, String relative) { - URI baseUri = uriInfo.getBaseUri(); - String uri = baseUri.getScheme() + "://" + baseUri.getHost(); - if (baseUri.getPort() != -1) { - uri += ":" + baseUri.getPort(); + private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) { + if (rootUrl == null) { + URI baseUri = uriInfo.getBaseUri(); + String uri = baseUri.getScheme() + "://" + baseUri.getHost(); + if (baseUri.getPort() != -1) { + uri += ":" + baseUri.getPort(); + } + rootUrl = uri; } - relative = uri + relative; + relative = rootUrl + relative; return relative; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java new file mode 100644 index 0000000000..5684ef6b47 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java @@ -0,0 +1,30 @@ +package org.keycloak.protocol.oidc.utils; + +import org.keycloak.models.ClientModel; +import org.keycloak.util.UriUtils; + +import javax.ws.rs.core.UriInfo; +import java.util.Set; + +/** + * Created by st on 22.09.15. + */ +public class WebOriginsUtils { + + public static final String INCLUDE_REDIRECTS = "+"; + + public static Set resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) { + Set webOrigins = client.getWebOrigins(); + if (webOrigins != null && webOrigins.contains("+")) { + webOrigins.remove(INCLUDE_REDIRECTS); + client.getRedirectUris(); + for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) { + if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) { + webOrigins.add(UriUtils.getOrigin(redirectUri)); + } + } + } + return webOrigins; + } + +} diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 3790d5a4f7..5f1f9bfba7 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.managers; import org.jboss.logging.Logger; @@ -49,7 +65,9 @@ import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Set; +import org.keycloak.freemarker.LocaleHelper; /** * Stateless object that manages authentication @@ -393,6 +411,9 @@ public class AuthenticationManager { } } } + + handleLoginLocale(realm, userSession, request, uriInfo); + // refresh the cookies! createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN); @@ -406,6 +427,17 @@ public class AuthenticationManager { } + // If a locale has been set on the login screen, associate that locale with the user + private static void handleLoginLocale(RealmModel realm, UserSessionModel userSession, + HttpRequest request, UriInfo uriInfo) { + Cookie localeCookie = request.getHttpHeaders().getCookies().get(LocaleHelper.LOCALE_COOKIE); + if (localeCookie == null) return; + + UserModel user = userSession.getUser(); + Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, request.getHttpHeaders()); + user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag()); + } + public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 8a94b6e8ae..d2014d5215 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -46,8 +46,8 @@ public class ResourceAdminManager { this.session = session; } - public static String resolveUri(URI requestUri, String uri) { - String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, uri); + public static String resolveUri(URI requestUri, String rootUrl, String uri) { + String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, rootUrl, uri); return StringPropertyReplacer.replaceProperties(absoluteURI); } @@ -58,8 +58,7 @@ public class ResourceAdminManager { return null; } - // this is to support relative admin urls when keycloak and clients are deployed on the same machine - String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, mgmtUrl); + String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), mgmtUrl); // this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine and avoid request to LB in cluster environment return StringPropertyReplacer.replaceProperties(absoluteURI); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 3b6edea190..20e7b6eb97 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -779,7 +779,7 @@ public class AccountService extends AbstractSecuredLocalService { if (referrerUri != null) { referrerUri = RedirectUtils.verifyRedirectUri(uriInfo, referrerUri, realm, referrerClient); } else { - referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), referrerClient.getBaseUrl()); + referrerUri = ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), referrerClient.getBaseUrl()); } if (referrerUri != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java b/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java new file mode 100644 index 0000000000..87e55b8b69 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/ClientRegistrationService.java @@ -0,0 +1,100 @@ +package org.keycloak.services.resources; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.BadRequestException; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.events.EventBuilder; +import org.keycloak.exportimport.ClientDescriptionConverter; +import org.keycloak.exportimport.KeycloakClientDescriptionConverter; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.models.utils.RepresentationToModel; +import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.services.ErrorResponse; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; + +/** + * @author Stian Thorgersen + */ +public class ClientRegistrationService { + + protected static final Logger logger = Logger.getLogger(ClientRegistrationService.class); + + private RealmModel realm; + + private EventBuilder event; + + @Context + private KeycloakSession session; + + public ClientRegistrationService(RealmModel realm, EventBuilder event) { + this.realm = realm; + this.event = event; + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) + public Response create(String description, @QueryParam("format") String format) { + if (format == null) { + format = KeycloakClientDescriptionConverter.ID; + } + + ClientDescriptionConverter converter = session.getProvider(ClientDescriptionConverter.class, format); + if (converter == null) { + throw new BadRequestException("Invalid format"); + } + ClientRepresentation rep = converter.convertToInternal(description); + + try { + ClientModel clientModel = RepresentationToModel.createClient(session, realm, rep, true); + rep = ModelToRepresentation.toRepresentation(clientModel); + URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(clientModel.getId()).build(); + return Response.created(uri).entity(rep).build(); + } catch (ModelDuplicateException e) { + return ErrorResponse.exists("Client " + rep.getClientId() + " already exists"); + } + } + + @GET + @Path("{clientId}") + @Produces(MediaType.APPLICATION_JSON) + public ClientRepresentation get(@PathParam("clientId") String clientId) { + AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, realm); + ClientModel client = clientAuth.getClient(); + if (client == null) { + throw new NotFoundException("Client not found"); + } + return ModelToRepresentation.toRepresentation(client); + } + + @PUT + @Path("{clientId}") + @Consumes(MediaType.APPLICATION_JSON) + public void update(@PathParam("clientId") String clientId, ClientRepresentation rep) { + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + throw new NotFoundException("Client not found"); + } + RepresentationToModel.updateClient(rep, client); + } + + @DELETE + @Path("{clientId}") + public void delete(@PathParam("clientId") String clientId) { + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + throw new NotFoundException("Client not found"); + } + realm.removeClient(client.getId()); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 083eb9a467..52f49df1ce 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -112,6 +112,15 @@ public class RealmsResource { return service; } +// @Path("{realm}/client-registration") +// public ClientRegistrationService getClientsService(final @PathParam("realm") String name) { +// RealmModel realm = init(name); +// EventBuilder event = new EventBuilder(realm, session, clientConnection); +// ClientRegistrationService service = new ClientRegistrationService(realm, event); +// ResteasyProviderFactory.getInstance().injectProperties(service); +// return service; +// } + @Path("{realm}/clients-managements") public ClientsManagementService getClientsManagementService(final @PathParam("realm") String name) { RealmModel realm = init(name); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index e3bde40340..2f845e0a8c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -44,7 +44,9 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; +import javax.ws.rs.QueryParam; /** * @author Bill Burke @@ -283,8 +285,7 @@ public class AdminConsole { map.put("resourceUrl", Urls.themeRoot(baseUri) + "/admin/" + adminTheme); map.put("resourceVersion", Version.RESOURCES_VERSION); - ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); - Theme theme = themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN); + Theme theme = getTheme(); map.put("properties", theme.getProperties()); @@ -296,10 +297,38 @@ public class AdminConsole { } } + private Theme getTheme() throws IOException { + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); + return themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN); + } + @GET @Path("{indexhtml: index.html}") // this expression is a hack to get around jaxdoclet generation bug. Doesn't like index.html public Response getIndexHtmlRedirect() { return Response.status(302).location(uriInfo.getRequestUriBuilder().path("../").build()).build(); } + @GET + @Path("messages.json") + @Produces(MediaType.APPLICATION_JSON) + public Properties getMessages(@QueryParam("lang") String lang) { + if (lang == null) { + logger.warn("Locale not specified for messages.json"); + lang = "en"; + } + + try { + Properties msgs = AdminMessagesLoader.getMessages(getTheme(), lang); + if (msgs.isEmpty()) { + logger.warn("Message bundle not found for language code '" + lang + "'"); + msgs = AdminMessagesLoader.getMessages(getTheme(), "en"); // fall back to en + } + + if (msgs.isEmpty()) logger.fatal("Message bundle not found for language code 'en'"); + + return msgs; + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java new file mode 100644 index 0000000000..43dfc3ebda --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.resources.admin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import org.keycloak.freemarker.Theme; + +/** + * Simple loader and cache for message bundles consumed by angular-translate. + * + * Note that these bundles are converted to JSON before being shipped to the UI. + * Also, the content should be formatted such that it can be interpolated by + * angular-translate. This is somewhat different from an ordinary Java bundle. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class AdminMessagesLoader { + private static final Map allMessages = new HashMap(); + + static Properties getMessages(Theme theme, String strLocale) throws IOException { + String allMessagesKey = theme.getName() + "_" + strLocale; + Properties messages = allMessages.get(allMessagesKey); + if (messages != null) return messages; + + Locale locale = Locale.forLanguageTag(strLocale); + messages = theme.getMessages("admin-messages", locale); + if (messages == null) return new Properties(); + + allMessages.put(allMessagesKey, messages); + return messages; + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index fda0ee3ac1..82dc0bd595 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -13,7 +13,8 @@ import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.events.admin.AdminEventQuery; import org.keycloak.events.admin.OperationType; -import org.keycloak.exportimport.ClientImporter; +import org.keycloak.exportimport.ClientDescriptionConverter; +import org.keycloak.exportimport.ClientDescriptionConverterFactory; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -25,7 +26,9 @@ import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.adapters.action.GlobalRequestResult; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.AuthenticationManager; @@ -99,10 +102,18 @@ public class RealmAdminResource { * * @return */ - @Path("client-importers/{formatId}") - public Object getClientImporter(@PathParam("formatId") String formatId) { - ClientImporter importer = session.getProvider(ClientImporter.class, formatId); - return importer.createJaxrsService(realm, auth); + @Path("client-description-converter") + @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) + @POST + @Produces(MediaType.APPLICATION_JSON) + public ClientRepresentation convertClientDescription(String description) { + for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(ClientDescriptionConverter.class)) { + if (((ClientDescriptionConverterFactory) factory).isSupported(description)) { + return factory.create(session).convertToInternal(description); + } + } + + throw new BadRequestException("Unsupported format"); } /** diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 70022c0c21..4ef2a630e1 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -4,8 +4,6 @@ import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; -import org.keycloak.exportimport.ClientImporter; -import org.keycloak.exportimport.ClientImporterFactory; import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.models.KeycloakSession; @@ -51,7 +49,6 @@ public class ServerInfoAdminResource { setSocialProviders(info); setIdentityProviders(info); setThemes(info); - setClientImporters(info); setProviders(info); setProtocolMapperTypes(info); setBuiltinProtocolMappers(info); @@ -144,7 +141,7 @@ public class ServerInfoAdminResource { ProtocolMapper mapper = (ProtocolMapper)p; List types = info.getProtocolMapperTypes().get(mapper.getProtocol()); if (types == null) { - types = new LinkedList(); + types = new LinkedList<>(); info.getProtocolMapperTypes().put(mapper.getProtocol(), types); } ProtocolMapperTypeRepresentation rep = new ProtocolMapperTypeRepresentation(); @@ -179,17 +176,6 @@ public class ServerInfoAdminResource { } } - private void setClientImporters(ServerInfoRepresentation info) { - info.setClientImporters(new LinkedList>()); - for (ProviderFactory p : session.getKeycloakSessionFactory().getProviderFactories(ClientImporter.class)) { - ClientImporterFactory factory = (ClientImporterFactory)p; - Map data = new HashMap(); - data.put("id", factory.getId()); - data.put("name", factory.getDisplayName()); - info.getClientImporters().add(data); - } - } - private static Map> createEnumsMap(Class... enums) { Map> m = new HashMap<>(); for (Class e : enums) { diff --git a/services/src/main/java/org/keycloak/services/util/ResolveRelative.java b/services/src/main/java/org/keycloak/services/util/ResolveRelative.java index 0954f735ed..757ff5879e 100755 --- a/services/src/main/java/org/keycloak/services/util/ResolveRelative.java +++ b/services/src/main/java/org/keycloak/services/util/ResolveRelative.java @@ -8,13 +8,17 @@ import java.net.URI; * @version $Revision: 1 $ */ public class ResolveRelative { - public static String resolveRelativeUri(URI requestUri, String url) { + public static String resolveRelativeUri(URI requestUri, String rootUrl, String url) { if (url == null || !url.startsWith("/")) return url; - UriBuilder builder = UriBuilder.fromPath(url).host(requestUri.getHost()); - builder.scheme(requestUri.getScheme()); - if (requestUri.getPort() != -1) { - builder.port(requestUri.getPort()); + if (rootUrl != null) { + return rootUrl + url; + } else { + UriBuilder builder = UriBuilder.fromPath(url).host(requestUri.getHost()); + builder.scheme(requestUri.getScheme()); + if (requestUri.getPort() != -1) { + builder.port(requestUri.getPort()); + } + return builder.build().toString(); } - return builder.build().toString(); } } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory b/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory new file mode 100644 index 0000000000..139c7b167f --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.exportimport.ClientDescriptionConverterFactory @@ -0,0 +1,2 @@ +org.keycloak.exportimport.KeycloakClientDescriptionConverter +org.keycloak.protocol.oidc.OIDCClientDescriptionConverter \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 0a9b272812..6d88f97303 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -1,6 +1,6 @@ org.keycloak.protocol.LoginProtocolSpi org.keycloak.protocol.ProtocolMapperSpi -org.keycloak.exportimport.ClientImportSpi +org.keycloak.exportimport.ClientDescriptionConverterSpi org.keycloak.wellknown.WellKnownSpi org.keycloak.messages.MessagesSpi org.keycloak.authentication.AuthenticatorSpi diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Bindings.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Bindings.java index 6e8d4a453e..9ef1b83e45 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Bindings.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Bindings.java @@ -7,7 +7,7 @@ import org.openqa.selenium.support.ui.Select; /** * Created by mhajas on 8/21/15. */ -public class Bindings extends Authentication{ +public class Bindings extends Authentication { @Override public String getUriFragment() { @@ -17,48 +17,48 @@ public class Bindings extends Authentication{ @FindBy(id = "browser") private Select BrowserFlowSelect; + @FindBy(id = "registration") + private Select RegistrationFlowSelect; + + @FindBy(id = "grant") + private Select DirectGrantFlowSelect; + + @FindBy(id = "resetCredentials") + private Select ResetCredentialsSelect; + + @FindBy(id = "clientAuthentication") + private Select ClientAuthenticationSelect; + + @FindBy(xpath = "//button[text()='Save']") + private WebElement saveButton; + + @FindBy(xpath = "//button[text()='Cancel']") + private WebElement cancelButton; + public void changeBrowserFlowSelect(BrowserFlowSelectValues value) { BrowserFlowSelect.selectByVisibleText(value.getName()); } - @FindBy(id = "registration") - private Select RegistrationFlowSelect; - public void changeRegistrationFlowSelect(RegistrationFlowSelectValues value) { RegistrationFlowSelect.selectByVisibleText(value.getName()); } - @FindBy(id = "grant") - private Select DirectGrantFlowSelect; - public void changeDirectGrantFlowSelect(DirectGrantFlowSelectValues value) { DirectGrantFlowSelect.selectByVisibleText(value.getName()); } - @FindBy(id = "resetCredentials") - private Select ResetCredentialsSelect; - public void changeResetCredentialsSelect(ResetCredentialsSelectValues value) { ResetCredentialsSelect.selectByVisibleText(value.getName()); } - @FindBy(id = "clientAuthentication") - private Select ClientAuthenticationSelect; - public void changeClientAuthenticationSelect(ClientAuthenticationSelectValues value) { ClientAuthenticationSelect.selectByVisibleText(value.getName()); } - @FindBy(xpath = "//button[text()='Save']") - private WebElement saveButton; - public void clickSave() { saveButton.click(); } - @FindBy(xpath = "//button[text()='Cancel']") - private WebElement cancelButton; - public void clickCancel() { cancelButton.click(); } @@ -143,5 +143,4 @@ public class Bindings extends Authentication{ } - } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Flows.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Flows.java index 199406440c..5aaa807adb 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Flows.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/Flows.java @@ -19,9 +19,14 @@ public class Flows extends Authentication { @FindBy(tagName = "select") private Select flowSelect; - public void changeFlowSelect(FlowSelectValues value) { - flowSelect.selectByVisibleText(value.getName()); - } + @FindBy(linkText = "New") + private WebElement newButton; + + @FindBy(linkText = "Copy") + private WebElement copyButton; + + @FindBy(tagName = "table") + private WebElement flowsTable; public enum FlowSelectValues { @@ -37,13 +42,12 @@ public class Flows extends Authentication { public String getName() { return name; } + } - @FindBy(linkText = "New") - private WebElement newButton; - - @FindBy(linkText = "Copy") - private WebElement copyButton; + public void changeFlowSelect(FlowSelectValues value) { + flowSelect.selectByVisibleText(value.getName()); + } public void clickNew() { newButton.click(); @@ -53,160 +57,156 @@ public class Flows extends Authentication { copyButton.click(); } + private void clickRadioButton(String row, int column) { + flowsTable.findElement(By.xpath("//td[text()[contains(.,'" + row + "')]]/../td[" + String.valueOf(column) + "]//input[@type='radio']")).click(); + } + // Direct grant public void setPasswordRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Password')]]/../td[2]//input[@type='radio']")).click(); + clickRadioButton("Password", 2); } public void setPasswordDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Password')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Password", 3); } public void setOTPRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'O T P')]]/../td[2]//input[@type='radio']")).click(); + clickRadioButton("O T P", 2); } public void setOTPOptional() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'O T P')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("O T P", 3); } public void setOTPDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'O T P')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("O T P", 4); } // Registration public void setRegistrationFormRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Registration form')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Registration form", 3); } public void setRegistrationFormDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Registration form')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Registration form", 4); } public void setRegistrationUserCreationRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Registration User Creation')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Registration User Creation", 3); } public void setRegistrationUserCreationDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Registration User Creation')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Registration User Creation", 4); } public void setProfileValidationRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Profile Validation')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Profile Validation", 3); } public void setProfileValidationDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Profile Validation')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Profile Validation", 4); } public void setPasswordValidationRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Password Validation')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Password Validation", 3); } public void setPasswordValidationDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Password Validation')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Password Validation", 4); } public void setRecaptchaRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Recaptcha')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Recaptcha", 3); } public void setRecaptchaDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Recaptcha')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Recaptcha", 4); } // Browser public void setCookieAlternative() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Cookie')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Cookie", 3); } public void setCookieDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Cookie')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Cookie", 4); } public void setKerberosAlternative() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Kerberos')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Kerberos", 3); } public void setKerberosRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Kerberos')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Kerberos", 4); } public void setKerberosDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Kerberos')]]/../td[5]//input[@type='radio']")).click(); + clickRadioButton("Kerberos", 5); } public void setFormsAlternative() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Forms')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Forms", 3); } public void setFormsRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Forms')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Forms", 4); } public void setFormsDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Forms')]]/../td[5]//input[@type='radio']")).click(); + clickRadioButton("Forms", 5); } public void setOTPFormRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' O T P Form')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton(" O T P Form", 3); } public void setOTPFormOptional() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' O T P Form')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton(" O T P Form", 4); } public void setOTPFormDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' O T P Form')]]/../td[5]//input[@type='radio']")).click(); + clickRadioButton(" O T P Form", 5); } // Reset credentials public void setResetPasswordRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset Password')]]/../td[2]//input[@type='radio']")).click(); + clickRadioButton("Reset Password", 2); } public void setResetPasswordOptional() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset Password')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Reset Password", 3); } public void setResetPasswordDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset Password')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Reset Password", 4); } public void setResetOTPRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset O T P')]]/../td[2]//input[@type='radio']")).click(); + clickRadioButton("Reset O T P", 2); } public void setResetOTPOptional() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset O T P')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Reset O T P", 3); } public void setResetOTPDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Reset O T P')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton("Reset O T P", 4); } // Clients - public void setClientIdAndSecretRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Client Id and Secret')]]/../td[2]//input[@type='radio']")).click(); - } - public void setClientIdAndSecretAlternative() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Client Id and Secret')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton("Client Id and Secret", 2); } public void setClientIdAndSecretDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,'Client Id and Secret')]]/../td[4]//input[@type='radio']")).click(); - } - - public void setSignedJwtRequired() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' Signed Jwt')]]/../td[2]//input[@type='radio']")).click(); + clickRadioButton("Client Id and Secret", 3); } public void setSignedJwtAlternative() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' Signed Jwt')]]/../td[3]//input[@type='radio']")).click(); + clickRadioButton(" Signed Jwt", 2); } public void setSignedJwtDisabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()[contains(.,' Signed Jwt')]]/../td[4]//input[@type='radio']")).click(); + clickRadioButton(" Signed Jwt", 3); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/OTPPolicy.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/OTPPolicy.java index 34e8a098d9..2a2bfdb57e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/OTPPolicy.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/OTPPolicy.java @@ -19,20 +19,20 @@ public class OTPPolicy extends Authentication { @FindBy(linkText = "Cancel") private WebElement cancelButton; + @FindBy(id = "lookAhead") + private WebElement lookAheadInput; + + @FindBy(id = "counter") + private WebElement initialCounterInput; + public void clickCancel() { cancelButton.click(); } - @FindBy(id = "lookAhead") - private WebElement lookAheadInput; - public void setLookAheadInputValue(String value) { Form.setInputValue(lookAheadInput, value); } - @FindBy(id = "counter") - private WebElement initialCounterInput; - public void setInitialcounterInputValue(String value) { Form.setInputValue(initialCounterInput, value); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java index b2b08b5493..3df1e679d6 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/PasswordPolicy.java @@ -1,14 +1,15 @@ package org.keycloak.testsuite.console.page.authentication; -import java.util.List; - import org.jboss.arquillian.graphene.findby.ByJQuery; -import static org.keycloak.testsuite.util.WaitUtils.waitGuiForElement; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.ui.Select; +import java.util.List; + +import static org.keycloak.testsuite.util.WaitUtils.waitGuiForElement; + /** * @author Petr Mensik * @author tkyjovsk @@ -23,7 +24,7 @@ public class PasswordPolicy extends Authentication { @FindBy(tagName = "select") private Select addPolicySelect; - + @FindBy(tagName = "select") private WebElement addPolicySelectElement; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java index 5da1cdaaed..0ea6af2b51 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/authentication/RequiredActions.java @@ -1,6 +1,8 @@ package org.keycloak.testsuite.console.page.authentication; import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; /** * @author tkyjovsk @@ -8,48 +10,62 @@ import org.openqa.selenium.By; */ public class RequiredActions extends Authentication { + public final static String ENABLED = "enabled"; + public final static String DEFAULT_ACTION = "defaultAction"; + + @FindBy(tagName = "table") + private WebElement requiredActionTable; + @Override public String getUriFragment() { return super.getUriFragment() + "/required-actions"; } - public void clickTermsAndConditionEnabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Terms and Conditions']/..//input[@type='checkbox' and @ng-model='requiredAction.enabled']")).click(); + private void setRequiredActionValue(String row, String column, boolean value) { + WebElement checkbox = requiredActionTable.findElement(By.xpath("//td[text()='" + row + "']/..//input[@ng-model='requiredAction." + column + "']")); + + if (checkbox.isSelected() != value) { + checkbox.click(); + } } - public void clickTermsAndConditionDefaultAction() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Terms and Conditions']/..//input[@type='checkbox' and @ng-model='requiredAction.defaultAction']")).click(); + public void setTermsAndConditionEnabled(boolean value) { + setRequiredActionValue("Terms and Conditions", ENABLED, value); } - public void clickVerifyEmailEnabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Verify Email']/..//input[@type='checkbox' and @ng-model='requiredAction.enabled']")).click(); + public void setTermsAndConditionDefaultAction(boolean value) { + setRequiredActionValue("Terms and Conditions", DEFAULT_ACTION, value); } - public void clickVerifyEmailDefaultAction() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Verify Email']/..//input[@type='checkbox' and @ng-model='requiredAction.defaultAction']")).click(); + public void setVerifyEmailEnabled(boolean value) { + setRequiredActionValue("Verify Email", ENABLED, value); } - public void clickUpdatePasswordEnabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Update Password']/..//input[@type='checkbox' and @ng-model='requiredAction.enabled']")).click(); + public void setVerifyEmailDefaultAction(boolean value) { + setRequiredActionValue("Verify Email", DEFAULT_ACTION, value); } - public void clickUpdatePasswordDefaultAction() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Update Password']/..//input[@type='checkbox' and @ng-model='requiredAction.defaultAction']")).click(); + public void setUpdatePasswordEnabled(boolean value) { + setRequiredActionValue("Update Password", ENABLED, value); } - public void clickConfigureTotpEnabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Configure Totp']/..//input[@type='checkbox' and @ng-model='requiredAction.enabled']")).click(); + public void setUpdatePasswordDefaultAction(boolean value) { + setRequiredActionValue("Update Password", DEFAULT_ACTION, value); } - public void clickConfigureTotpDefaultAction() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Configure Totp']/..//input[@type='checkbox' and @ng-model='requiredAction.defaultAction']")).click(); + public void setConfigureTotpEnabled(boolean value) { + setRequiredActionValue("Configure Totp", ENABLED, value); } - public void clickUpdateProfileEnabled() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Update Profile']/..//input[@type='checkbox' and @ng-model='requiredAction.enabled']")).click(); + public void setConfigureTotpDefaultAction(boolean value) { + setRequiredActionValue("Configure Totp", DEFAULT_ACTION, value); } - public void clickUpdateProfileDefaultAction() { - driver.findElement(By.xpath("//td[@class='ng-binding' and text()='Update Profile']/..//input[@type='checkbox' and @ng-model='requiredAction.defaultAction']")).click(); + public void setUpdateProfileEnabled(boolean value) { + setRequiredActionValue("Update Profile", ENABLED, value); + } + + public void setUpdateProfileDefaultAction(boolean value) { + setRequiredActionValue("Update Profile", DEFAULT_ACTION, value); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/AdminEvents.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/AdminEvents.java index 08e0e0006f..8cf2afc534 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/AdminEvents.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/AdminEvents.java @@ -7,7 +7,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; /** - * * @author tkyjovsk * @author mhajas */ @@ -27,6 +26,12 @@ public class AdminEvents extends Events { public class AdminEventsTable extends DataTable { + @FindBy(xpath = "//button[text()[contains(.,'Filter')]]") + private WebElement filterButton; + + @FindBy(tagName = "form") + private AdminEventsTableFilterForm filterForm; + public void update() { waitAjaxForBody(); clickHeaderButton("Update"); @@ -37,63 +42,63 @@ public class AdminEvents extends Events { clickHeaderButton("Reset"); } - @FindBy(xpath = "//button[text()[contains(.,'Filter')]]") - private WebElement filterButton; - public void filter() { waitAjaxForBody(); filterButton.click(); } - @FindBy(tagName = "form") - private AdminEventsTableFilterForm filterForm; - public AdminEventsTableFilterForm filterForm() { return filterForm; } public class AdminEventsTableFilterForm extends Form { + @FindBy(id = "resource") + private WebElement resourcePathInput; + + @FindBy(id = "realm") + private WebElement realmInput; + + @FindBy(id = "client") + private WebElement clientInput; + + @FindBy(id = "user") + private WebElement userInput; + + @FindBy(id = "ipAddress") + private WebElement ipAddressInput; + + @FindBy(xpath = "//div[@id='s2id_adminEnabledEventOperations']/ul") + private WebElement operationTypesInput; + + @FindBy(xpath = "//div[@id='select2-drop']") + private WebElement operationTypesValues; + public void addOperationType(String type) { - driver.findElement(By.xpath("//div[@id='s2id_adminEnabledEventOperations']/ul")).click(); - driver.findElement(By.xpath("//div[@id='select2-drop']//div[text()[contains(.,'" + type + "')]]/..")).click(); + operationTypesInput.click(); + operationTypesValues.findElement(By.xpath("//div[text() = '" + type + "']")).click(); } public void removeOperationType(String type) { - driver.findElement(By.xpath("//div[@id='s2id_adminEnabledEventOperations']//div[text()='" + type + "']/../a")).click(); + operationTypesInput.findElement(By.xpath("//div[text()='" + type + "']/../a")).click(); } - @FindBy(id = "resource") - private WebElement resourcePathInput; - public void setResourcePathInput(String value) { setInputValue(resourcePathInput, value); } - @FindBy(id = "realm") - private WebElement realmInput; - public void setRealmInput(String value) { setInputValue(realmInput, value); } - @FindBy(id = "client") - private WebElement clientInput; - public void setClientInput(String value) { setInputValue(clientInput, value); } - @FindBy(id = "user") - private WebElement userInput; - public void setUserInput(String value) { setInputValue(userInput, value); } - @FindBy(id = "ipAddress") - private WebElement ipAddressInput; - public void setIpAddressInput(String value) { setInputValue(ipAddressInput, value); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/Config.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/Config.java index 686e902b19..be84772e0f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/Config.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/Config.java @@ -32,6 +32,33 @@ public class Config extends Events { @FindBy(xpath = "//div[@id='s2id_autogen1']/..//select") private Select eventListenersSelect; + @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='enabled']]") + private OnOffSwitch SaveEvents; + + @FindBy(xpath = "//div[@id='s2id_enabledEventTypes']//input") + private WebElement savedTypesInput; + + @FindBy(xpath = "//div[@id='select2-drop']/ul") + private WebElement savedTypesOptions; + + @FindBy(id = "expiration") + private WebElement expirationInput; + + @FindBy(name = "expirationUnit") + private Select expirationUnitSelect; + + @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='adminEventsEnabled']]") + private OnOffSwitch saveAdminEvents; + + @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='adminEventsDetailsEnabled']]") + private OnOffSwitch includeRepresentation; + + @FindBy(xpath = "//button[@data-ng-click='clearEvents()']") + private WebElement clearLoginEventsButton; + + @FindBy(xpath = "//button[@data-ng-click='clearAdminEvents()']") + private WebElement clearAdminEventsButton; + public void addEventListener(String listener) { eventListenersInput.click(); eventListenersSelect.selectByVisibleText(listener); @@ -41,19 +68,10 @@ public class Config extends Events { eventListenersInput.findElement(By.xpath("//div[text()='" + listener + "']/../a")).click(); } - @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='enabled']]") - private OnOffSwitch SaveEvents; - public void setSaveEvents(boolean value) { SaveEvents.setOn(value); } - @FindBy(xpath = "//div[@id='s2id_enabledEventTypes']//input") - private WebElement savedTypesInput; - - @FindBy(xpath = "//div[@id='select2-drop']/ul") - private WebElement savedTypesOptions; - public void addSaveType(String type) { savedTypesInput.click(); savedTypesOptions.findElement(By.xpath("//div[text()='" + type + "']")).click(); @@ -64,36 +82,24 @@ public class Config extends Events { } public void clearLoginEvents() { - driver.findElement(By.xpath("//button[@data-ng-click='clearEvents()']")).click(); + clearLoginEventsButton.click(); } - @FindBy(id = "expiration") - private WebElement expirationInput; - - @FindBy(name = "expirationUnit") - private Select expirationUnitSelect; - public void setExpiration(String value, String unit) { expirationUnitSelect.selectByVisibleText(unit); Form.setInputValue(expirationInput, value); } - @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='adminEventsEnabled']]") - private OnOffSwitch saveAdminEvents; - public void setSaveAdminEvents(boolean value) { saveAdminEvents.setOn(value); } - @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='adminEventsDetailsEnabled']]") - private OnOffSwitch includeRepresentation; - public void setIncludeRepresentation(boolean value) { includeRepresentation.setOn(value); } public void clearAdminEvents() { - driver.findElement(By.xpath("//button[@data-ng-click='clearAdminEvents()']")).click(); + clearAdminEventsButton.click(); } } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/LoginEvents.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/LoginEvents.java index 6752951139..136e455a8e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/LoginEvents.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/events/LoginEvents.java @@ -26,6 +26,12 @@ public class LoginEvents extends Events { public class LoginEventsTable extends DataTable { + @FindBy(xpath = "//button[text()[contains(.,'Filter')]]") + private WebElement filterButton; + + @FindBy(tagName = "form") + private LoginEventsTableFilterForm filterForm; + public void update() { waitAjaxForBody(); clickHeaderButton("Update"); @@ -36,42 +42,42 @@ public class LoginEvents extends Events { clickHeaderButton("Reset"); } - @FindBy(xpath = "//button[text()[contains(.,'Filter')]]") - private WebElement filterButton; - public void filter() { waitAjaxForBody(); filterButton.click(); } - @FindBy(tagName = "form") - private LoginEventsTableFilterForm filterForm; - public LoginEventsTableFilterForm filterForm() { return filterForm; } public class LoginEventsTableFilterForm extends Form { + @FindBy(id = "client") + private WebElement clientInput; + + @FindBy(id = "user") + private WebElement userInput; + + @FindBy(xpath = "//div[@id='s2id_eventTypes']/ul") + private WebElement eventTypeInput; + + @FindBy(xpath = "//div[@id='select2-drop']") + private WebElement eventTypeValues; + public void addEventType(String type) { - driver.findElement(By.xpath("//div[@id='s2id_eventTypes']/ul")).click(); - driver.findElement(By.xpath("//div[@id='select2-drop']//div[text()='" + type + "']/..")).click(); + eventTypeInput.click(); + eventTypeValues.findElement(By.xpath("//div[text()='" + type + "']")).click(); } public void removeOperationType(String type) { - driver.findElement(By.xpath("//div[@id='s2id_eventTypes']//div[text()='" + type + "']/../a")).click(); + eventTypeInput.findElement(By.xpath("//div[text()='" + type + "']/../a]")).click(); } - @FindBy(id = "client") - private WebElement clientInput; - public void setClientInput(String value) { setInputValue(clientInput, value); } - @FindBy(id = "user") - private WebElement userInput; - public void setUserInput(String value) { setInputValue(userInput, value); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java new file mode 100644 index 0000000000..75e54b2087 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/BruteForceDetection.java @@ -0,0 +1,123 @@ +package org.keycloak.testsuite.console.page.realm; + +import org.jboss.arquillian.graphene.findby.FindByJQuery; +import org.jboss.arquillian.graphene.page.Page; +import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; +import org.keycloak.testsuite.page.Form; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.ui.Select; + +/** + * @author mhajas + */ +public class BruteForceDetection extends SecurityDefenses { + + @Override + public String getUriFragment() { + return super.getUriFragment() + "/brute-force"; + } + + @Page + private BruteForceDetectionForm form; + + public BruteForceDetectionForm form() { + return form; + } + + public class BruteForceDetectionForm extends Form { + + @FindByJQuery("div[class='onoffswitch']") + private OnOffSwitch protectionEnabled; + + @FindBy(id = "failureFactor") + private WebElement maxLoginFailures; + + @FindBy(id = "waitIncrement") + private WebElement waitIncrementInput; + + @FindBy(name = "waitIncrementUnit") + private Select waitIncrementSelect; + + @FindBy(id = "quickLoginCheckMilliSeconds") + private WebElement quickLoginCheckInput; + + @FindBy(id = "minimumQuickLoginWait") + private WebElement minQuickLoginWaitInput; + + @FindBy(name = "minimumQuickLoginWaitUnit") + private Select minQuickLoginWaitSelect; + + @FindBy(id = "maxFailureWait") + private WebElement maxWaitInput; + + @FindBy(name = "maxFailureWaitUnit") + private Select maxWaitSelect; + + @FindBy(id = "maxDeltaTime") + private WebElement failureResetTimeInput; + + @FindBy(name = "maxDeltaTimeUnit") + private Select failureResetTimeSelect; + + public void setProtectionEnabled(boolean protectionEnabled) { + this.protectionEnabled.setOn(protectionEnabled); + } + + public void setMaxLoginFailures(String value) { + setInputValue(maxLoginFailures, value); + } + + public void setWaitIncrementInput(String value) { + setInputValue(waitIncrementInput, value); + } + + public void setWaitIncrementSelect(TimeSelectValues value) { + waitIncrementSelect.selectByVisibleText(value.getName()); + } + + public void setQuickLoginCheckInput(String value) { + setInputValue(quickLoginCheckInput, value); + } + + public void setMinQuickLoginWaitInput(String value) { + setInputValue(minQuickLoginWaitInput, value); + } + + public void setMinQuickLoginWaitSelect(TimeSelectValues value) { + minQuickLoginWaitSelect.selectByVisibleText(value.getName()); + } + + public void setMaxWaitInput(String value) { + setInputValue(maxWaitInput, value); + } + + public void setMaxWaitSelect(TimeSelectValues value) { + maxWaitSelect.selectByVisibleText(value.getName()); + } + + public void setFailureResetTimeInput(String value) { + setInputValue(failureResetTimeInput, value); + } + + public void setFailureResetTimeSelect(TimeSelectValues value) { + failureResetTimeSelect.selectByVisibleText(value.getName()); + } + } + + public enum TimeSelectValues { + + SECONDS("Seconds"), MINUTES("Minutes"), HOURS("Hours"), DAYS("Days"); + + private String name; + + private TimeSelectValues(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/EmailSettings.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/EmailSettings.java index f75117b54a..130174ef09 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/EmailSettings.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/EmailSettings.java @@ -11,7 +11,7 @@ import org.openqa.selenium.support.FindBy; * Created by mhajas on 8/25/15. */ public class EmailSettings extends RealmSettings { - + @Override public String getUriFragment() { return super.getUriFragment() + "/smtp-settings"; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/Headers.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/Headers.java new file mode 100644 index 0000000000..d8c161bc4f --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/Headers.java @@ -0,0 +1,41 @@ +package org.keycloak.testsuite.console.page.realm; + +import org.jboss.arquillian.graphene.page.Page; +import org.keycloak.testsuite.page.Form; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author mhajas + */ +public class Headers extends SecurityDefenses { + + @Override + public String getUriFragment() { + return super.getUriFragment() + "/headers"; + } + + @Page + private HeadersForm form; + + public HeadersForm form() { + return form; + } + + public class HeadersForm extends Form { + + @FindBy(id = "xFrameOptions") + private WebElement xFrameOptions; + + @FindBy(id = "contentSecurityPolicy") + private WebElement contentSecurityPolicy; + + public void setXFrameOptions(String value) { + setInputValue(xFrameOptions, value); + } + + public void setContentSecurityPolicy(String value) { + setInputValue(contentSecurityPolicy, value); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/SecurityDefenses.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/SecurityDefenses.java index 00bd0b8414..24120af32d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/SecurityDefenses.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/console/page/realm/SecurityDefenses.java @@ -18,13 +18,7 @@ package org.keycloak.testsuite.console.page.realm; import org.jboss.arquillian.graphene.findby.FindByJQuery; -import org.jboss.arquillian.graphene.page.Page; -import org.keycloak.testsuite.console.page.fragment.OnOffSwitch; -import org.keycloak.testsuite.page.Form; -import static org.keycloak.testsuite.page.Form.setInputValue; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.ui.Select; /** * @author Filip Kiss @@ -37,160 +31,16 @@ public class SecurityDefenses extends RealmSettings { return super.getUriFragment() + "/defense"; // NOTE: page doesn't exist, only subpages } - public class Headers extends SecurityDefenses { - - @Override - public String getUriFragment() { - return super.getUriFragment() + "/headers"; - } - - @Page - private HeadersForm form; - - public HeadersForm form() { - return form; - } - - public class HeadersForm extends Form { - - @FindBy(id = "xFrameOptions") - private WebElement xFrameOptions; - - public void setXFrameOptions(String value) { - setInputValue(xFrameOptions, value); - } - - @FindBy(id = "contentSecurityPolicy") - private WebElement contentSecurityPolicy; - - public void setContentSecurityPolicy(String value) { - setInputValue(contentSecurityPolicy, value); - } - } - } - - public enum TimeSelectValues { - - SECONDS("Seconds"), MINUTES("Minutes"), HOURS("Hours"), DAYS("Days"); - - private String name; - - private TimeSelectValues(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - public class BruteForceDetection extends SecurityDefenses { - - @Override - public String getUriFragment() { - return super.getUriFragment() + "/brute-force"; - } - - @Page - private BruteForceDetectionForm form; - - public BruteForceDetectionForm form() { - return form; - } - - public class BruteForceDetectionForm extends Form { - - @FindByJQuery("div[class='onoffswitch']") - private OnOffSwitch protectionEnabled; - - public void setProtectionEnabled(boolean protectionEnabled) { - this.protectionEnabled.setOn(protectionEnabled); - } - - @FindBy(id = "failureFactor") - private WebElement maxLoginFailures; - - public void setMaxLoginFailures(String value) { - setInputValue(maxLoginFailures, value); - } - - @FindBy(id = "waitIncrement") - private WebElement waitIncrementInput; - - @FindBy(name = "waitIncrementUnit") - private Select waitIncrementSelect; - - public void setWaitIncrementInput(String value) { - setInputValue(waitIncrementInput, value); - } - - public void setWaitIncrementSelect(TimeSelectValues value) { - waitIncrementSelect.selectByVisibleText(value.getName()); - } - - @FindBy(id = "quickLoginCheckMilliSeconds") - private WebElement quickLoginCheckInput; - - public void setQuickLoginCheckInput(String value) { - setInputValue(quickLoginCheckInput, value); - } - - @FindBy(id = "minimumQuickLoginWait") - private WebElement minQuickLoginWaitInput; - - @FindBy(name = "minimumQuickLoginWaitUnit") - private Select minQuickLoginWaitSelect; - - public void setMinQuickLoginWaitInput(String value) { - setInputValue(minQuickLoginWaitInput, value); - } - - public void setMinQuickLoginWaitSelect(TimeSelectValues value) { - minQuickLoginWaitSelect.selectByVisibleText(value.getName()); - } - - @FindBy(id = "maxFailureWait") - private WebElement maxWaitInput; - - @FindBy(name = "maxFailureWaitUnit") - private Select maxWaitSelect; - - public void setMaxWaitInput(String value) { - setInputValue(maxWaitInput, value); - } - - public void setMaxWaitSelect(TimeSelectValues value) { - maxWaitSelect.selectByVisibleText(value.getName()); - } - - @FindBy(id = "maxDeltaTime") - private WebElement failureResetTimeInput; - - @FindBy(name = "maxDeltaTimeUnit") - private Select failureResetTimeSelect; - - public void setFailureResetTimeInput(String value) { - setInputValue(failureResetTimeInput, value); - } - - public void setFailureResetTimeSelect(TimeSelectValues value) { - failureResetTimeSelect.selectByVisibleText(value.getName()); - } - - } - - } - @FindByJQuery("a:contains('Brute Force Detection')") private WebElement bruteForceDetectionTab; + @FindByJQuery("a:contains('Headers')") + private WebElement headersTab; + public void goToBruteForceDetection() { bruteForceDetectionTab.click(); } - @FindByJQuery("a:contains('Headers')") - private WebElement headersTab; - public void goToHeaders() { headersTab.click(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java index 75cf70f9e2..77e9a4e5fa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java @@ -22,9 +22,9 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.testsuite.console.AbstractConsoleTest; import org.keycloak.testsuite.console.page.authentication.PasswordPolicy; +import org.keycloak.testsuite.console.page.users.UserCredentials; import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy.Type.*; -import org.keycloak.testsuite.console.page.users.UserCredentials; /** * @author Petr Mensik diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java index 9c818d140e..c45b792660 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/RequiredActionsTest.java @@ -19,7 +19,6 @@ package org.keycloak.testsuite.console.authentication; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.auth.page.login.Registration; @@ -51,49 +50,17 @@ public class RequiredActionsTest extends AbstractConsoleTest { super.setDefaultPageUriParameters(); testRealmRegistrationPage.setAuthRealm("test"); } - + @Before public void beforeRequiredActionsTest() { requiredActionsPage.navigateTo(); } - @Test - public void requiredActionsTest() { - requiredActionsPage.clickTermsAndConditionEnabled(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickTermsAndConditionDefaultAction(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickVerifyEmailEnabled(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickVerifyEmailDefaultAction(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickUpdatePasswordEnabled(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickUpdatePasswordDefaultAction(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickConfigureTotpEnabled(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickConfigureTotpDefaultAction(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickUpdateProfileEnabled(); - assertFlashMessageSuccess(); - - requiredActionsPage.clickUpdateProfileDefaultAction(); - assertFlashMessageSuccess(); - } - @Test public void termsAndConditionsDefaultActionTest() { - requiredActionsPage.clickTermsAndConditionEnabled(); - requiredActionsPage.clickTermsAndConditionDefaultAction(); + requiredActionsPage.setTermsAndConditionEnabled(true); + requiredActionsPage.setTermsAndConditionDefaultAction(true); + assertFlashMessageSuccess(); allowTestRealmUserRegistration(); @@ -106,7 +73,8 @@ public class RequiredActionsTest extends AbstractConsoleTest { @Test public void configureTotpDefaultActionTest() { - requiredActionsPage.clickConfigureTotpDefaultAction(); + requiredActionsPage.setConfigureTotpDefaultAction(true); + assertFlashMessageSuccess(); allowTestRealmUserRegistration(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java index 84a9930245..0263513d57 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/SecurityDefensesTest.java @@ -21,7 +21,7 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; import org.keycloak.testsuite.auth.page.account.Account; -import org.keycloak.testsuite.console.page.realm.SecurityDefenses; +import org.keycloak.testsuite.console.page.realm.BruteForceDetection; import org.keycloak.testsuite.console.page.users.UserAttributes; import org.keycloak.testsuite.console.page.users.Users; import org.openqa.selenium.By; @@ -41,7 +41,7 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; public class SecurityDefensesTest extends AbstractRealmTest { @Page - private SecurityDefenses.BruteForceDetection bruteForceDetectionPage; + private BruteForceDetection bruteForceDetectionPage; @Page private Account testRealmAccountPage; @@ -69,7 +69,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("1"); - bruteForceDetectionPage.form().setWaitIncrementSelect(SecurityDefenses.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setWaitIncrementSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setWaitIncrementInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); assertFlashMessageSuccess(); @@ -110,7 +110,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("100"); bruteForceDetectionPage.form().setQuickLoginCheckInput("1500"); - bruteForceDetectionPage.form().setMinQuickLoginWaitSelect(SecurityDefenses.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setMinQuickLoginWaitSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setMinQuickLoginWaitInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); assertFlashMessageSuccess(); @@ -150,7 +150,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("1"); - bruteForceDetectionPage.form().setMaxWaitSelect(SecurityDefenses.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setMaxWaitSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setMaxWaitInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); @@ -190,7 +190,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("2"); - bruteForceDetectionPage.form().setFailureResetTimeSelect(SecurityDefenses.TimeSelectValues.SECONDS); + bruteForceDetectionPage.form().setFailureResetTimeSelect(BruteForceDetection.TimeSelectValues.SECONDS); bruteForceDetectionPage.form().setFailureResetTimeInput(String.valueOf(secondsToWait)); bruteForceDetectionPage.form().save(); assertFlashMessageSuccess(); @@ -223,7 +223,7 @@ public class SecurityDefensesTest extends AbstractRealmTest { public void userUnlockTest() { bruteForceDetectionPage.form().setProtectionEnabled(true); bruteForceDetectionPage.form().setMaxLoginFailures("1"); - bruteForceDetectionPage.form().setWaitIncrementSelect(SecurityDefenses.TimeSelectValues.MINUTES); + bruteForceDetectionPage.form().setWaitIncrementSelect(BruteForceDetection.TimeSelectValues.MINUTES); bruteForceDetectionPage.form().setWaitIncrementInput("10"); bruteForceDetectionPage.form().save(); assertFlashMessageSuccess(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/users/RequiredUserActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/users/RequiredUserActionsTest.java index 9151698d89..d532ae6363 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/users/RequiredUserActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/users/RequiredUserActionsTest.java @@ -1,28 +1,24 @@ package org.keycloak.testsuite.console.users; -import static org.jboss.arquillian.graphene.Graphene.waitGui; import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Test; -import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; -import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; import org.keycloak.testsuite.auth.page.account.Account; import org.keycloak.testsuite.auth.page.login.UpdateAccount; import org.keycloak.testsuite.auth.page.login.UpdatePassword; import org.keycloak.testsuite.console.page.authentication.RequiredActions; import org.keycloak.testsuite.console.page.users.UserAttributes; - -import static org.keycloak.testsuite.model.RequiredUserAction.TERMS_AND_CONDITIONS; -import static org.keycloak.testsuite.model.RequiredUserAction.UPDATE_PASSWORD; -import static org.keycloak.testsuite.model.RequiredUserAction.UPDATE_PROFILE; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; - import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; +import static org.jboss.arquillian.graphene.Graphene.waitGui; +import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; +import static org.keycloak.testsuite.model.RequiredUserAction.*; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; + /** - * * @author tkyjovsk * @author mhajas */ @@ -124,7 +120,7 @@ public class RequiredUserActionsTest extends AbstractUserTest { @Test public void termsAndConditions() { requiredActionsPage.navigateTo(); - requiredActionsPage.clickTermsAndConditionEnabled(); + requiredActionsPage.setTermsAndConditionEnabled(true); manage().users(); usersPage.table().viewAllUsers(); @@ -142,6 +138,4 @@ public class RequiredUserActionsTest extends AbstractUserTest { } - - } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java index aaed86e578..deda8f89b4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java @@ -177,6 +177,7 @@ public class AdminAPITest { if (appRep.isBearerOnly() != null) Assert.assertEquals(appRep.isBearerOnly(), storedApp.isBearerOnly()); if (appRep.isPublicClient() != null) Assert.assertEquals(appRep.isPublicClient(), storedApp.isPublicClient()); if (appRep.isFullScopeAllowed() != null) Assert.assertEquals(appRep.isFullScopeAllowed(), storedApp.isFullScopeAllowed()); + if (appRep.getRootUrl() != null) Assert.assertEquals(appRep.getRootUrl(), storedApp.getRootUrl()); if (appRep.getAdminUrl() != null) Assert.assertEquals(appRep.getAdminUrl(), storedApp.getAdminUrl()); if (appRep.getBaseUrl() != null) Assert.assertEquals(appRep.getBaseUrl(), storedApp.getBaseUrl()); if (appRep.isSurrogateAuthRequired() != null) Assert.assertEquals(appRep.isSurrogateAuthRequired(), storedApp.isSurrogateAuthRequired()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java index 8208aa2fb7..8c41dfa2ef 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java @@ -1,13 +1,18 @@ package org.keycloak.testsuite.admin; +import org.apache.commons.io.IOUtils; import org.junit.Test; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.managers.RealmManager; +import org.keycloak.util.JsonSerialization; import javax.ws.rs.NotFoundException; +import java.io.IOException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -132,4 +137,35 @@ public class RealmTest extends AbstractClientTest { } } + @Test + public void convertKeycloakClientDescription() throws IOException { + ClientRepresentation description = new ClientRepresentation(); + description.setClientId("client-id"); + description.setRedirectUris(Collections.singletonList("http://localhost")); + + ClientRepresentation converted = realm.convertClientDescription(JsonSerialization.writeValueAsString(description)); + assertEquals("client-id", converted.getClientId()); + assertEquals("http://localhost", converted.getRedirectUris().get(0)); + } + + @Test + public void convertOIDCClientDescription() throws IOException { + String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/client-oidc.json")); + + ClientRepresentation converted = realm.convertClientDescription(description); + assertEquals(36, converted.getClientId().length()); + assertEquals(1, converted.getRedirectUris().size()); + assertEquals("http://localhost", converted.getRedirectUris().get(0)); + } + + @Test + public void convertSAMLClientDescription() throws IOException { + String description = IOUtils.toString(getClass().getResourceAsStream("/client-descriptions/saml-entity-descriptor.xml")); + + ClientRepresentation converted = realm.convertClientDescription(description); + assertEquals("loadbalancer-9.siroe.com", converted.getClientId()); + assertEquals(1, converted.getRedirectUris().size()); + assertEquals("https://LoadBalancer-9.siroe.com:3443/federation/Consumer/metaAlias/sp", converted.getRedirectUris().get(0)); + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java index 358fd23382..f97a05ebd7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTest.java @@ -30,7 +30,7 @@ public class SamlAdapterTest { initializeSamlSecuredWar("/keycloak-saml/bad-client-signed-post", "/bad-client-sales-post-sig", "bad-client-post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/bad-realm-signed-post", "/bad-realm-sales-post-sig", "bad-realm-post-sig.war", classLoader); initializeSamlSecuredWar("/keycloak-saml/encrypted-post", "/sales-post-enc", "post-enc.war", classLoader); - SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", this); + SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth"); server.getServer().deploy(createDeploymentInfo("employee.war", "/employee", SamlSPFacade.class)); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java index 6e0817b1c0..2c32d5161f 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.keycloaksaml; +import org.apache.commons.io.IOUtils; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.junit.Assert; import org.junit.ClassRule; @@ -8,6 +9,8 @@ import org.junit.Test; import org.junit.rules.ExternalResource; import org.keycloak.Config; import org.keycloak.adapters.saml.SamlPrincipal; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -24,6 +27,7 @@ import org.keycloak.protocol.saml.mappers.HardcodedRole; import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.RoleNameMapper; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.saml.processing.core.saml.v2.constants.X500SAMLProfileConstants; import org.keycloak.services.managers.RealmManager; @@ -34,6 +38,7 @@ import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.util.JsonSerialization; import org.openqa.selenium.WebDriver; import javax.ws.rs.client.Client; @@ -51,6 +56,8 @@ import java.io.InputStream; import java.util.LinkedList; import java.util.List; +import static org.junit.Assert.assertEquals; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -106,9 +113,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostSimpleLoginLogout() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); System.out.println(driver.getPageSource()); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true"); @@ -117,9 +124,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostSimpleUnauthorized(CheckAuthError error) { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("unauthorized", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); System.out.println(driver.getPageSource()); error.check(driver); } @@ -127,7 +134,7 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostSimpleLoginLogoutIdpInitiated() { driver.navigate().to(AUTH_SERVER_URL + "/realms/demo/protocol/saml/clients/sales-post"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post/"); System.out.println(driver.getPageSource()); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post?GLO=true"); @@ -136,9 +143,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostSignedLoginLogout() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig?GLO=true"); checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post-sig/"); @@ -146,9 +153,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { } public void testPostSignedLoginLogoutTransientNameID() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-transient/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-transient/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-transient/"); System.out.println(driver.getPageSource()); Assert.assertFalse(driver.getPageSource().contains("bburke")); Assert.assertTrue(driver.getPageSource().contains("principal=G-")); @@ -158,9 +165,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { } public void testPostSignedLoginLogoutPersistentNameID() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-persistent/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-persistent/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-persistent/"); System.out.println(driver.getPageSource()); Assert.assertFalse(driver.getPageSource().contains("bburke")); Assert.assertTrue(driver.getPageSource().contains("principal=G-")); @@ -170,9 +177,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { } public void testPostSignedLoginLogoutEmailNameID() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-email/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-email/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig-email/"); System.out.println(driver.getPageSource()); Assert.assertTrue(driver.getPageSource().contains("principal=bburke@redhat.com")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig-email?GLO=true"); @@ -188,8 +195,8 @@ public class SamlAdapterTestStrategy extends ExternalResource { Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml")); System.out.println(driver.getCurrentUrl()); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee/"); - Assert.assertEquals(SamlSPFacade.sentRelayState, SamlSPFacade.RELAY_STATE); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee/"); + assertEquals(SamlSPFacade.sentRelayState, SamlSPFacade.RELAY_STATE); Assert.assertNotNull(SamlSPFacade.samlResponse); } @@ -206,13 +213,13 @@ public class SamlAdapterTestStrategy extends ExternalResource { requiredRoles.add("user"); SendUsernameServlet.checkRoles = requiredRoles; loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/"); SendUsernameServlet.checkRoles = null; SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal; Assert.assertNotNull(principal); - Assert.assertEquals("bburke@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get())); - Assert.assertEquals("bburke@redhat.com", principal.getFriendlyAttribute("email")); - Assert.assertEquals("617", principal.getAttribute("phone")); + assertEquals("bburke@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get())); + assertEquals("bburke@redhat.com", principal.getFriendlyAttribute("email")); + assertEquals("617", principal.getAttribute("phone")); Assert.assertNull(principal.getFriendlyAttribute("phone")); driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/?GLO=true"); checkLoggedOut(APP_SERVER_BASE_URL + "/employee2/"); @@ -252,11 +259,11 @@ public class SamlAdapterTestStrategy extends ExternalResource { requiredRoles.add("pee-on"); SendUsernameServlet.checkRoles = requiredRoles; loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/"); SendUsernameServlet.checkRoles = null; SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal; Assert.assertNotNull(principal); - Assert.assertEquals("hard", principal.getAttribute("hardcoded-attribute")); + assertEquals("hard", principal.getAttribute("hardcoded-attribute")); } @@ -266,7 +273,7 @@ public class SamlAdapterTestStrategy extends ExternalResource { driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig/"); Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml")); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig?GLO=true"); checkLoggedOut(APP_SERVER_BASE_URL + "/employee-sig/"); @@ -277,7 +284,7 @@ public class SamlAdapterTestStrategy extends ExternalResource { driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front/"); Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml")); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front?GLO=true"); checkLoggedOut(APP_SERVER_BASE_URL + "/employee-sig-front/"); @@ -291,19 +298,19 @@ public class SamlAdapterTestStrategy extends ExternalResource { Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml")); System.out.println("login to form"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); // visit 2nd app System.out.println("visit 2nd app "); driver.navigate().to(APP_SERVER_BASE_URL + "/employee-sig-front/"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee-sig-front/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); // visit 3rd app System.out.println("visit 3rd app "); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-sig/"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-sig/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); // logout of first app @@ -320,9 +327,9 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostEncryptedLoginLogout() { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-enc/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-enc/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-post-enc/"); Assert.assertTrue(driver.getPageSource().contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-post-enc?GLO=true"); checkLoggedOut(APP_SERVER_BASE_URL + "/sales-post-enc/"); @@ -330,8 +337,8 @@ public class SamlAdapterTestStrategy extends ExternalResource { } public void testPostBadClientSignature() { driver.navigate().to(APP_SERVER_BASE_URL + "/bad-client-sales-post-sig/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); - Assert.assertEquals(driver.getTitle(), "We're sorry..."); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getTitle(), "We're sorry..."); } public static interface CheckAuthError { @@ -340,39 +347,19 @@ public class SamlAdapterTestStrategy extends ExternalResource { public void testPostBadRealmSignature(CheckAuthError error) { driver.navigate().to(APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/bad-realm-sales-post-sig/"); System.out.println(driver.getPageSource()); error.check(driver); } - private static String createToken(String AUTH_SERVER_URL, AbstractKeycloakRule keycloakRule) { - KeycloakSession session = keycloakRule.startSession(); - try { - RealmManager manager = new RealmManager(session); - - RealmModel adminRealm = manager.getRealm(Config.getAdminRealm()); - ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); - TokenManager tm = new TokenManager(); - UserModel admin = session.users().getUserByUsername("admin", adminRealm); - ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole); - clientSession.setNote(OIDCLoginProtocol.ISSUER, AUTH_SERVER_URL + "/realms/master"); - UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null); - AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, true, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession); - return tm.encodeToken(adminRealm, token); - } finally { - keycloakRule.stopSession(session, true); - } - } - - public void testMetadataPostSignedLoginLogout() throws Exception { driver.navigate().to(APP_SERVER_BASE_URL + "/sales-metadata/"); - Assert.assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); + assertEquals(driver.getCurrentUrl(), AUTH_SERVER_URL + "/realms/demo/protocol/saml"); loginPage.login("bburke", "password"); - Assert.assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-metadata/"); + assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/sales-metadata/"); String pageSource = driver.getPageSource(); Assert.assertTrue(pageSource.contains("bburke")); driver.navigate().to(APP_SERVER_BASE_URL + "/sales-metadata?GLO=true"); @@ -380,30 +367,21 @@ public class SamlAdapterTestStrategy extends ExternalResource { } - public static void uploadSP(String AUTH_SERVER_URL, AbstractKeycloakRule keycloakRule) { - String token = createToken(AUTH_SERVER_URL, keycloakRule); - final String authHeader = "Bearer " + token; - ClientRequestFilter authFilter = new ClientRequestFilter() { - @Override - public void filter(ClientRequestContext requestContext) throws IOException { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); - } - }; - Client client = ClientBuilder.newBuilder().register(authFilter).build(); - UriBuilder authBase = UriBuilder.fromUri(AUTH_SERVER_URL + ""); - WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase)); + public static void uploadSP(String AUTH_SERVER_URL) { + try { + Keycloak keycloak = Keycloak.getInstance(AUTH_SERVER_URL, "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID, null); + RealmResource admin = keycloak.realm("demo"); + admin.toRepresentation(); - MultipartFormDataOutput formData = new MultipartFormDataOutput(); - InputStream is = SamlAdapterTestStrategy.class.getResourceAsStream("/keycloak-saml/sp-metadata.xml"); - Assert.assertNotNull(is); - formData.addFormData("file", is, MediaType.APPLICATION_XML_TYPE); + ClientRepresentation clientRep = admin.convertClientDescription(IOUtils.toString(SamlAdapterTestStrategy.class.getResourceAsStream("/keycloak-saml/sp-metadata.xml"))); + Response response = admin.clients().create(clientRep); - WebTarget upload = adminRealms.path("demo/client-importers/saml2-entity-descriptor/upload"); - System.out.println(upload.getUri()); - Response response = upload.request().post(Entity.entity(formData, MediaType.MULTIPART_FORM_DATA)); - Assert.assertEquals(204, response.getStatus()); - response.close(); - client.close(); + assertEquals(201, response.getStatus()); + + keycloak.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java index 2d2ab44bb2..019bfea94e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java @@ -1,11 +1,14 @@ package org.keycloak.testsuite.saml; +import org.apache.commons.io.IOUtils; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.Config; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -22,9 +25,11 @@ import org.keycloak.protocol.saml.mappers.HardcodedRole; import org.keycloak.protocol.saml.mappers.RoleListMapper; import org.keycloak.protocol.saml.mappers.RoleNameMapper; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminRoot; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; @@ -56,6 +61,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import static org.junit.Assert.assertEquals; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -480,30 +487,21 @@ public class SamlBindingTest { } public static void uploadSP() { - String token = createToken(); - final String authHeader = "Bearer " + token; - ClientRequestFilter authFilter = new ClientRequestFilter() { - @Override - public void filter(ClientRequestContext requestContext) throws IOException { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); - } - }; - Client client = ClientBuilder.newBuilder().register(authFilter).build(); - UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth"); - WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase)); + try { + Keycloak keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID, null); + RealmResource admin = keycloak.realm("demo"); + admin.toRepresentation(); - MultipartFormDataOutput formData = new MultipartFormDataOutput(); - InputStream is = SamlBindingTest.class.getResourceAsStream("/saml/sp-metadata.xml"); - Assert.assertNotNull(is); - formData.addFormData("file", is, MediaType.APPLICATION_XML_TYPE); + ClientRepresentation clientRep = admin.convertClientDescription(IOUtils.toString(SamlBindingTest.class.getResourceAsStream("/saml/sp-metadata.xml"))); + Response response = admin.clients().create(clientRep); - WebTarget upload = adminRealms.path("demo/client-importers/saml2-entity-descriptor/upload"); - System.out.println(upload.getUri()); - Response response = upload.request().post(Entity.entity(formData, MediaType.MULTIPART_FORM_DATA)); - Assert.assertEquals(204, response.getStatus()); - response.close(); - client.close(); + assertEquals(201, response.getStatus()); + + keycloak.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } } diff --git a/testsuite/integration/src/test/resources/client-descriptions/client-oidc.json b/testsuite/integration/src/test/resources/client-descriptions/client-oidc.json new file mode 100644 index 0000000000..51562fd361 --- /dev/null +++ b/testsuite/integration/src/test/resources/client-descriptions/client-oidc.json @@ -0,0 +1,6 @@ +{ + "client_name": "Name", + "redirect_uris": [ + "http://localhost" + ] +} \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/client-descriptions/saml-entity-descriptor.xml b/testsuite/integration/src/test/resources/client-descriptions/saml-entity-descriptor.xml new file mode 100644 index 0000000000..b00ab251a4 --- /dev/null +++ b/testsuite/integration/src/test/resources/client-descriptions/saml-entity-descriptor.xml @@ -0,0 +1,82 @@ + + + + + + +MIICYDCCAgqgAwIBAgICBoowDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDIxOTExMzRaFw0xMDA3MjkxOTExMzRaMDcxEjAQBgNVBAoTCXNp +cm9lLmNvbTEhMB8GA1UEAxMYbG9hZGJhbGFuY2VyLTkuc2lyb2UuY29tMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCjOwa5qoaUuVnknqf5pdgAJSEoWlvx/jnUYbkSDpXLzraEiy2UhvwpoBgB +EeTSUaPPBvboCItchakPI6Z/aFdH3Wmjuij9XD8r1C+q//7sUO0IGn0ORycddHhoo0aSdnnxGf9V +tREaqKm9dJ7Yn7kQHjo2eryMgYxtr/Z5Il5F+wIDAQABo2AwXjARBglghkgBhvhCAQEEBAMCBkAw +DgYDVR0PAQH/BAQDAgTwMB8GA1UdIwQYMBaAFDugITflTCfsWyNLTXDl7cMDUKuuMBgGA1UdEQQR +MA+BDW1hbGxhQHN1bi5jb20wDQYJKoZIhvcNAQEEBQADQQB/6DOB6sRqCZu2OenM9eQR0gube85e +nTTxU4a7x1naFxzYXK1iQ1vMARKMjDb19QEJIEJKZlDK4uS7yMlf1nFS + + + + + + + + +MIICTDCCAfagAwIBAgICBo8wDQYJKoZIhvcNAQEEBQAwgZIxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +EwpDYWxpZm9ybmlhMRQwEgYDVQQHEwtTYW50YSBDbGFyYTEeMBwGA1UEChMVU3VuIE1pY3Jvc3lz +dGVtcyBJbmMuMRowGAYDVQQLExFJZGVudGl0eSBTZXJ2aWNlczEcMBoGA1UEAxMTQ2VydGlmaWNh +dGUgTWFuYWdlcjAeFw0wNjExMDcyMzU2MTdaFw0xMDA4MDMyMzU2MTdaMCMxITAfBgNVBAMTGGxv +YWRiYWxhbmNlci05LnNpcm9lLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAw574iRU6 +HsSO4LXW/OGTXyfsbGv6XRVOoy3v+J1pZ51KKejcDjDJXNkKGn3/356AwIaqbcymWd59T0zSqYfR +Hn+45uyjYxRBmVJseLpVnOXLub9jsjULfGx0yjH4w+KsZSZCXatoCHbj/RJtkzuZY6V9to/hkH3S +InQB4a3UAgMCAwEAAaNgMF4wEQYJYIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIE8DAfBgNV +HSMEGDAWgBQ7oCE35Uwn7FsjS01w5e3DA1CrrjAYBgNVHREEETAPgQ1tYWxsYUBzdW4uY29tMA0G +CSqGSIb3DQEBBAUAA0EAMlbfBg/ff0Xkv4DOR5LEqmfTZKqgdlD81cXynfzlF7XfnOqI6hPIA90I +x5Ql0ejivIJAYcMGUyA+/YwJg2FGoA== + + + + + 128 + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + diff --git a/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java index 7e317d377b..c72ab019d9 100755 --- a/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java +++ b/testsuite/tomcat6/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java @@ -30,7 +30,6 @@ import org.junit.Test; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; -import org.keycloak.testsuite.adapter.AdapterTestStrategy; import org.keycloak.testsuite.keycloaksaml.SamlAdapterTestStrategy; import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.openqa.selenium.WebDriver; @@ -73,7 +72,7 @@ public class TomcatSamlTest { tomcat.deploySaml("/bad-client-sales-post-sig", "bad-client-signed-post"); tomcat.deploySaml("/bad-realm-sales-post-sig", "bad-realm-signed-post"); tomcat.deploySaml("/sales-post-enc", "encrypted-post"); - SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule); + SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth"); tomcat.start();
ClientActive Sessions{{:: 'client' | translate}}{{:: 'active-sessions' | translate}}