From 8727aef6475bc85ad649eebc5e9ff77234cc8d16 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Fri, 27 Mar 2015 15:41:42 +0100 Subject: [PATCH 1/3] KEYCLOAK-1113 - LoginFormProvider extended to allow per field errors, freemarker implementation extended (backward compatible), used for registration form --- .../java/org/keycloak/login/FormMessage.java | 69 +++++++++++++++ .../keycloak/login/LoginFormsProvider.java | 28 ++++-- .../FreeMarkerLoginFormsProvider.java | 85 +++++++++++++------ .../login/freemarker/model/MessageBean.java | 9 ++ .../resources/LoginActionsService.java | 23 ++--- .../services/validation/Validation.java | 77 ++++++++++------- 6 files changed, 213 insertions(+), 78 deletions(-) create mode 100644 forms/login-api/src/main/java/org/keycloak/login/FormMessage.java diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java b/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java new file mode 100644 index 0000000000..707ad31295 --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java @@ -0,0 +1,69 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.login; + +import java.util.Arrays; + +/** + * Message (eg. error) to be shown in form. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class FormMessage { + + /** + * Value used for {@link #field} if message is global (not tied to any specific form field) + */ + public static final String GLOBAL = "global"; + + private String field; + private String message; + private Object[] parameters; + + /** + * Create message. + * + * @param field this message is for. {@link #GLOBAL} is used if null + * @param message key for the message + * @param parameters to be formatted into message + */ + public FormMessage(String field, String message, Object... parameters) { + this(field, message); + this.parameters = parameters; + } + + /** + * Create message without parameters. + * + * @param field this message is for. {@link #GLOBAL} is used if null + * @param message key for the message + */ + public FormMessage(String field, String message) { + super(); + if (field == null) + field = GLOBAL; + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } + + public Object[] getParameters() { + return parameters; + } + + @Override + public String toString() { + return "FormMessage [field=" + field + ", message=" + message + ", parameters=" + Arrays.toString(parameters) + "]"; + } + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 36a5252515..ac73087bca 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -1,5 +1,13 @@ package org.keycloak.login; +import java.net.URI; +import java.util.List; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; @@ -7,13 +15,6 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.List; - /** * @author Stian Thorgersen */ @@ -48,7 +49,20 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); public LoginFormsProvider setAccessRequest(String message); + /** + * Set one global error message. + * + * @param message key of message + * @param parameters to be formatted into message + */ public LoginFormsProvider setError(String message, Object ... parameters); + + /** + * Set multiple error messages. + * + * @param messages to be set + */ + public LoginFormsProvider setErrors(List messages); public LoginFormsProvider setSuccess(String message, Object ... parameters); 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 9aca77f006..6d600f060c 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 @@ -8,6 +8,7 @@ import org.keycloak.email.EmailProvider; import org.keycloak.freemarker.*; import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod; import org.keycloak.freemarker.beans.MessageFormatterMethod; +import org.keycloak.login.FormMessage; import org.keycloak.login.LoginFormsPages; import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.freemarker.model.ClientBean; @@ -32,6 +33,7 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Urls; import javax.ws.rs.core.*; + import java.io.IOException; import java.net.URI; import java.text.MessageFormat; @@ -55,9 +57,8 @@ import java.util.concurrent.TimeUnit; private Map httpResponseHeaders = new HashMap(); private String accessRequestMessage; private URI actionUri; - private Object[] parameters; - private String message; + private List messages = null; private MessageType messageType = MessageType.ERROR; private MultivaluedMap formData; @@ -134,7 +135,7 @@ import java.util.concurrent.TimeUnit; return Response.serverError().build(); } - if (message == null) { + if (messages == null) { setWarning(actionMessage); } @@ -175,24 +176,34 @@ import java.util.concurrent.TimeUnit; logger.warn("Failed to load properties", e); } - Properties messages; + Properties messagesBundle; Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders); try { - messages = theme.getMessages(locale); - attributes.put("msg", new MessageFormatterMethod(locale, messages)); + messagesBundle = theme.getMessages(locale); + attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); } catch (IOException e) { logger.warn("Failed to load messages", e); - messages = new Properties(); + messagesBundle = new Properties(); } - if (message != null) { - String formattedMessage; - if(messages.containsKey(message)){ - formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters); - }else{ - formattedMessage = message; + if (messages != null) { + Map messagesPerField = new HashMap(); + MessageBean wholeMessage = new MessageBean(null, messageType); + for (FormMessage message : this.messages) { + String formattedMessageText = formatMessageMessage(message, messagesBundle, locale); + if (formattedMessageText != null) { + wholeMessage.appendSummaryLine(formattedMessageText); + MessageBean fm = messagesPerField.get(message.getField()); + if (fm == null) { + messagesPerField.put(message.getField(), new MessageBean(formattedMessageText, messageType)); + } else { + fm.appendSummaryLine(formattedMessageText); + } + } } - attributes.put("message", new MessageBean(formattedMessage, messageType)); + + attributes.put("message", wholeMessage); + attributes.put("messagePerField", messagesPerField); } if (page == LoginFormsPages.OAUTH_GRANT) { // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param @@ -218,7 +229,7 @@ import java.util.concurrent.TimeUnit; b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath()); break; } - attributes.put("locale", new LocaleBean(realm, locale, b, messages)); + attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle)); } } @@ -240,10 +251,10 @@ import java.util.concurrent.TimeUnit; break; case OAUTH_GRANT: attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage)); - attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messages)); + attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle)); break; case CODE: - attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null)); + attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? getFirstMessageUnformatted() : null)); break; } @@ -303,24 +314,46 @@ import java.util.concurrent.TimeUnit; return createResponse(LoginFormsPages.CODE); } - public FreeMarkerLoginFormsProvider setError(String message, Object ... parameters) { - this.message = message; + protected void setMessage(MessageType type, String message, Object... parameters) { + messageType = type; + messages = new ArrayList<>(); + messages.add(new FormMessage(null, message, parameters)); + } + + protected String getFirstMessageUnformatted() { + if (messages != null && !messages.isEmpty()) { + return messages.get(0).getMessage(); + } + return null; + } + + protected String formatMessageMessage(FormMessage message, Properties messagesBundle, Locale locale) { + if (message == null) + return null; + if (messagesBundle.containsKey(message.getMessage())) { + return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale).format(message.getParameters()); + } else { + return message.getMessage(); + } + } + public FreeMarkerLoginFormsProvider setError(String message, Object... parameters) { + setMessage(MessageType.ERROR, message, parameters); + return this; + } + + public LoginFormsProvider setErrors(List messages) { this.messageType = MessageType.ERROR; - this.parameters = parameters; + this.messages = new ArrayList<>(messages); return this; } public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) { - this.message = message; - this.messageType = MessageType.SUCCESS; - this.parameters = parameters; + setMessage(MessageType.SUCCESS, message, parameters); return this; } public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) { - this.message = message; - this.messageType = MessageType.WARNING; - this.parameters = parameters; + setMessage(MessageType.WARNING, message, parameters); return this; } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java index 72d48b7d88..f5e69c4145 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java @@ -41,6 +41,15 @@ public class MessageBean { return summary; } + public void appendSummaryLine(String newLine) { + if (newLine == null) + return; + if (summary == null) + summary = newLine; + else + summary = summary + "
" + newLine; + } + public String getType() { return this.type.toString().toLowerCase(); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 5b405355a6..0a7e8b1df4 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -31,6 +31,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.login.FormMessage; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.*; import org.keycloak.models.UserModel.RequiredAction; @@ -63,6 +64,8 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; + +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -422,8 +425,8 @@ public class LoginActionsService { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_CODE); } - String username = formData.getFirst("username"); - String email = formData.getFirst("email"); + String username = formData.getFirst(Validation.FIELD_USERNAME); + String email = formData.getFirst(Validation.FIELD_EMAIL); if (realm.isRegistrationEmailAsUsername()) { username = email; formData.putSingle(AuthenticationManager.FORM_USERNAME, username); @@ -458,20 +461,12 @@ public class LoginActionsService { } // Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm - String errorMessage = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes); - Object[] parameters = new Object[0]; - if (errorMessage == null) { - PasswordPolicy.Error error = Validation.validatePassword(formData, realm.getPasswordPolicy()); - if(error != null){ - errorMessage = error.getMessage(); - parameters = error.getParameters(); - } - } + List errors = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes, realm.getPasswordPolicy()); - if (errorMessage != null) { + if (errors != null && !errors.isEmpty()) { event.error(Errors.INVALID_REGISTRATION); return Flows.forms(session, realm, client, uriInfo, headers) - .setError(errorMessage, parameters) + .setErrors(errors) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createRegistration(); @@ -488,7 +483,7 @@ public class LoginActionsService { } // Validate that user with this email doesn't exist in realm or any federation provider - if (session.users().getUserByEmail(email, realm) != null) { + if (email != null && session.users().getUserByEmail(email, realm) != null) { event.error(Errors.EMAIL_IN_USE); return Flows.forms(session, realm, client, uriInfo, headers) .setError(Messages.EMAIL_EXISTS) diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index f72a63023a..5fb98f9d96 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -1,71 +1,86 @@ package org.keycloak.services.validation; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import javax.ws.rs.core.MultivaluedMap; + +import org.keycloak.login.FormMessage; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.messages.Messages; -import javax.ws.rs.core.MultivaluedMap; -import java.util.List; -import java.util.regex.Pattern; - public class Validation { + public static final String FIELD_PASSWORD_CONFIRM = "password-confirm"; + public static final String FIELD_EMAIL = "email"; + public static final String FIELD_LAST_NAME = "lastName"; + public static final String FIELD_FIRST_NAME = "firstName"; + public static final String FIELD_PASSWORD = "password"; + public static final String FIELD_USERNAME = "username"; + // Actually allow same emails like angular. See ValidationTest.testEmailValidation() private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*"); - public static String validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes) { - if (isEmpty(formData.getFirst("firstName"))) { - return Messages.MISSING_FIRST_NAME; + public static List validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes, PasswordPolicy policy) { + List errors = new ArrayList<>(); + + if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst(FIELD_USERNAME))) { + addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME); } - if (isEmpty(formData.getFirst("lastName"))) { - return Messages.MISSING_LAST_NAME; + if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) { + addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); } - if (isEmpty(formData.getFirst("email"))) { - return Messages.MISSING_EMAIL; + if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) { + addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); } - if (!isEmailValid(formData.getFirst("email"))) { - return Messages.INVALID_EMAIL; - } - - if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst("username"))) { - return Messages.MISSING_USERNAME; + if (isEmpty(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); + } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); } if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { - if (isEmpty(formData.getFirst(CredentialRepresentation.PASSWORD))) { - return Messages.MISSING_PASSWORD; - } - - if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) { - return Messages.INVALID_PASSWORD_CONFIRM; + if (isEmpty(formData.getFirst(FIELD_PASSWORD))) { + addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD); + } else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) { + addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM); } } - return null; + if (formData.getFirst(FIELD_PASSWORD) != null) { + PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD)); + if (err != null) + errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters())); + } + + return errors; + } + + private static void addError(List errors, String field, String message){ + errors.add(new FormMessage(field, message)); } - public static PasswordPolicy.Error validatePassword(MultivaluedMap formData, PasswordPolicy policy) { - return policy.validate(formData.getFirst("username"), formData.getFirst("password")); - } public static String validateUpdateProfileForm(MultivaluedMap formData) { - if (isEmpty(formData.getFirst("firstName"))) { + if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) { return Messages.MISSING_FIRST_NAME; } - if (isEmpty(formData.getFirst("lastName"))) { + if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) { return Messages.MISSING_LAST_NAME; } - if (isEmpty(formData.getFirst("email"))) { + if (isEmpty(formData.getFirst(FIELD_EMAIL))) { return Messages.MISSING_EMAIL; } - if (!isEmailValid(formData.getFirst("email"))) { + if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { return Messages.INVALID_EMAIL; } From 31a80a973c780a9ca1f6c2386b4e479078dfbb7b Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 30 Mar 2015 09:05:48 +0200 Subject: [PATCH 2/3] KEYCLOAK-1113 - user registration form input boxes and labels are red on error (css class is configurable in theme, has-error used for patternfly) --- .../resources/theme/base/login/register.ftl | 12 +-- .../theme/patternfly/login/theme.properties | 1 + .../FreeMarkerLoginFormsProvider.java | 14 ++-- .../model/MessagesPerFieldBean.java | 76 +++++++++++++++++++ 4 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java diff --git a/forms/common-themes/src/main/resources/theme/base/login/register.ftl b/forms/common-themes/src/main/resources/theme/base/login/register.ftl index 930a07d13f..e560cf13d7 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/register.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/register.ftl @@ -7,7 +7,7 @@ <#elseif section = "form">
<#if !realm.registrationEmailAsUsername> -
+
@@ -16,7 +16,7 @@
-
+
@@ -25,7 +25,7 @@
-
+
@@ -34,7 +34,7 @@
-
+
@@ -43,7 +43,7 @@
-
+
@@ -52,7 +52,7 @@
-
+
diff --git a/forms/common-themes/src/main/resources/theme/patternfly/login/theme.properties b/forms/common-themes/src/main/resources/theme/patternfly/login/theme.properties index 25427a7167..7c83966cd7 100644 --- a/forms/common-themes/src/main/resources/theme/patternfly/login/theme.properties +++ b/forms/common-themes/src/main/resources/theme/patternfly/login/theme.properties @@ -18,6 +18,7 @@ kcFormAreaClass=col-xs-12 col-sm-8 col-md-8 col-lg-6 login kcFormClass=form-horizontal kcFormGroupClass=form-group +kcFormGroupErrorClass=has-error kcLabelClass=control-label kcLabelWrapperClass=col-xs-12 col-sm-12 col-md-4 col-lg-3 kcInputClass=form-control 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 6d600f060c..22fa3f7096 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 @@ -16,6 +16,7 @@ import org.keycloak.login.freemarker.model.CodeBean; import org.keycloak.freemarker.beans.LocaleBean; import org.keycloak.login.freemarker.model.LoginBean; import org.keycloak.login.freemarker.model.MessageBean; +import org.keycloak.login.freemarker.model.MessagesPerFieldBean; import org.keycloak.login.freemarker.model.OAuthGrantBean; import org.keycloak.login.freemarker.model.ProfileBean; import org.keycloak.login.freemarker.model.RealmBean; @@ -186,25 +187,20 @@ import java.util.concurrent.TimeUnit; messagesBundle = new Properties(); } + MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean(); if (messages != null) { - Map messagesPerField = new HashMap(); MessageBean wholeMessage = new MessageBean(null, messageType); for (FormMessage message : this.messages) { String formattedMessageText = formatMessageMessage(message, messagesBundle, locale); if (formattedMessageText != null) { wholeMessage.appendSummaryLine(formattedMessageText); - MessageBean fm = messagesPerField.get(message.getField()); - if (fm == null) { - messagesPerField.put(message.getField(), new MessageBean(formattedMessageText, messageType)); - } else { - fm.appendSummaryLine(formattedMessageText); - } + messagesPerField.addMessage(message.getField(), formattedMessageText, messageType); } } - attributes.put("message", wholeMessage); - attributes.put("messagePerField", messagesPerField); } + attributes.put("messagesPerField", messagesPerField); + if (page == LoginFormsPages.OAUTH_GRANT) { // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param uriBuilder.replaceQuery(null); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java new file mode 100644 index 0000000000..2384f16eb1 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java @@ -0,0 +1,76 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.login.freemarker.model; + +import java.util.HashMap; +import java.util.Map; + +import org.keycloak.login.FormMessage; +import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider.MessageType; + +/** + * Bean used to hold form messages per field. Stored under messagesPerField key in Freemarker context. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class MessagesPerFieldBean { + + private Map messagesPerField = new HashMap(); + + public void addMessage(String field, String messageText, MessageType messageType) { + if (messageText == null || messageText.trim().isEmpty()) + return; + if (field == null) + field = FormMessage.GLOBAL; + + MessageBean fm = messagesPerField.get(field); + if (fm == null) { + messagesPerField.put(field, new MessageBean(messageText, messageType)); + } else { + fm.appendSummaryLine(messageText); + } + } + + /** + * Check if message for given field exists + * + * @param field + * @return + */ + public boolean exists(String field) { + return messagesPerField.containsKey(field); + } + + /** + * Get message for given field. + * + * @param fieldName + * @return message text or empty string + */ + public String get(String fieldName) { + MessageBean mb = messagesPerField.get(fieldName); + if (mb != null) { + return mb.getSummary(); + } else { + return ""; + } + } + + /** + * Print text if message for given field exists. Useful eg. to add css styles for fields with message. + * + * @param fieldName to check for + * @param text to print + * @return text if message exists for given field, else empty string + */ + public String printIfExists(String fieldName, String text) { + if (exists(fieldName)) + return text; + else + return ""; + } + +} From 01094663bf3acfaa9b48e3da06aa3764fb3082a6 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 30 Mar 2015 12:53:05 +0200 Subject: [PATCH 3/3] KEYCLOAK-1113 - profile update now uses correct form validation also --- .../org/keycloak/account/AccountProvider.java | 49 ++++---- .../freemarker/FreeMarkerAccountProvider.java | 119 ++++++++++++------ .../account/freemarker/model/MessageBean.java | 60 --------- .../freemarker/beans}/MessageBean.java | 14 +-- .../freemarker/beans/MessageType.java | 17 +++ .../beans}/MessagesPerFieldBean.java | 7 +- .../resources/theme/base/account/account.ftl | 8 +- .../theme/base/login/login-update-profile.ftl | 6 +- .../keycloak/login/LoginFormsProvider.java | 1 + .../FreeMarkerLoginFormsProvider.java | 55 +++++--- .../keycloak/models/utils}/FormMessage.java | 2 +- .../services/resources/AccountService.java | 8 +- .../resources/LoginActionsService.java | 8 +- .../services/validation/Validation.java | 20 +-- 14 files changed, 200 insertions(+), 174 deletions(-) delete mode 100644 forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java rename forms/{login-freemarker/src/main/java/org/keycloak/login/freemarker/model => common-freemarker/src/main/java/org/keycloak/freemarker/beans}/MessageBean.java (77%) create mode 100644 forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageType.java rename forms/{login-freemarker/src/main/java/org/keycloak/login/freemarker/model => common-freemarker/src/main/java/org/keycloak/freemarker/beans}/MessagesPerFieldBean.java (90%) rename {forms/login-api/src/main/java/org/keycloak/login => model/api/src/main/java/org/keycloak/models/utils}/FormMessage.java (97%) diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java index bc6920b5fa..f5bcbb2838 100755 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java @@ -1,52 +1,55 @@ package org.keycloak.account; -import org.apache.http.client.methods.HttpHead; -import org.keycloak.events.Event; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.provider.Provider; +import java.util.List; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.util.List; + +import org.keycloak.events.Event; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.provider.Provider; /** * @author Stian Thorgersen */ public interface AccountProvider extends Provider { - AccountProvider setUriInfo(UriInfo uriInfo); + AccountProvider setUriInfo(UriInfo uriInfo); - AccountProvider setHttpHeaders(HttpHeaders httpHeaders); + AccountProvider setHttpHeaders(HttpHeaders httpHeaders); - Response createResponse(AccountPages page); + Response createResponse(AccountPages page); - AccountProvider setError(String message, Object ... parameters); + AccountProvider setError(String message, Object... parameters); - AccountProvider setSuccess(String message, Object ... parameters); + AccountProvider setErrors(List messages); - AccountProvider setWarning(String message, Object ... parameters); + AccountProvider setSuccess(String message, Object... parameters); - AccountProvider setUser(UserModel user); + AccountProvider setWarning(String message, Object... parameters); - AccountProvider setProfileFormData(MultivaluedMap formData); + AccountProvider setUser(UserModel user); - AccountProvider setStatus(Response.Status status); + AccountProvider setProfileFormData(MultivaluedMap formData); - AccountProvider setRealm(RealmModel realm); + AccountProvider setStatus(Response.Status status); - AccountProvider setReferrer(String[] referrer); + AccountProvider setRealm(RealmModel realm); - AccountProvider setEvents(List events); + AccountProvider setReferrer(String[] referrer); - AccountProvider setSessions(List sessions); + AccountProvider setEvents(List events); - AccountProvider setPasswordSet(boolean passwordSet); + AccountProvider setSessions(List sessions); - AccountProvider setStateChecker(String stateChecker); + AccountProvider setPasswordSet(boolean passwordSet); - AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported); + AccountProvider setStateChecker(String stateChecker); + + AccountProvider setFeatures(boolean social, boolean events, boolean passwordUpdateSupported); } 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 4e0c007deb..282853197d 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,25 +1,54 @@ package org.keycloak.account.freemarker; +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + import org.jboss.logging.Logger; import org.keycloak.account.AccountPages; import org.keycloak.account.AccountProvider; -import org.keycloak.account.freemarker.model.*; +import org.keycloak.account.freemarker.model.AccountBean; +import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean; +import org.keycloak.account.freemarker.model.FeaturesBean; +import org.keycloak.account.freemarker.model.LogBean; +import org.keycloak.account.freemarker.model.PasswordBean; +import org.keycloak.account.freemarker.model.RealmBean; +import org.keycloak.account.freemarker.model.ReferrerBean; +import org.keycloak.account.freemarker.model.SessionsBean; +import org.keycloak.account.freemarker.model.TotpBean; +import org.keycloak.account.freemarker.model.UrlBean; import org.keycloak.events.Event; -import org.keycloak.freemarker.*; +import org.keycloak.freemarker.BrowserSecurityHeaderSetup; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.LocaleHelper; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.freemarker.beans.LocaleBean; +import org.keycloak.freemarker.beans.MessageBean; import org.keycloak.freemarker.beans.MessageFormatterMethod; +import org.keycloak.freemarker.beans.MessageType; +import org.keycloak.freemarker.beans.MessagesPerFieldBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.services.resources.flows.Urls; -import javax.ws.rs.core.*; -import java.io.IOException; -import java.net.URI; -import java.text.MessageFormat; -import java.util.*; -import org.keycloak.freemarker.beans.LocaleBean; - /** * @author Stian Thorgersen */ @@ -43,13 +72,10 @@ public class FreeMarkerAccountProvider implements AccountProvider { private FreeMarkerUtil freeMarker; private HttpHeaders headers; - public static enum MessageType {SUCCESS, WARNING, ERROR} - private UriInfo uriInfo; - private String message; - private Object[] parameters; - private MessageType messageType; + private List messages = null; + private MessageType messageType = MessageType.ERROR; public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { this.session = session; @@ -87,13 +113,13 @@ public class FreeMarkerAccountProvider implements AccountProvider { } Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers); - Properties messages; + Properties messagesBundle; try { - messages = theme.getMessages(locale); - attributes.put("msg", new MessageFormatterMethod(locale, messages)); + messagesBundle = theme.getMessages(locale); + attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); } catch (IOException e) { logger.warn("Failed to load messages", e); - messages = new Properties(); + messagesBundle = new Properties(); } URI baseUri = uriInfo.getBaseUri(); @@ -107,15 +133,19 @@ public class FreeMarkerAccountProvider implements AccountProvider { attributes.put("stateChecker", stateChecker); } - if (message != null) { - String formattedMessage; - if(messages.containsKey(message)){ - formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters); - }else{ - formattedMessage = message; + MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean(); + if (messages != null) { + MessageBean wholeMessage = new MessageBean(null, messageType); + for (FormMessage message : this.messages) { + String formattedMessageText = formatMessage(message, messagesBundle, locale); + if (formattedMessageText != null) { + wholeMessage.appendSummaryLine(formattedMessageText); + messagesPerField.addMessage(message.getField(), formattedMessageText, messageType); + } } - attributes.put("message", new MessageBean(formattedMessage, messageType)); + attributes.put("message", wholeMessage); } + attributes.put("messagesPerField", messagesPerField); if (referrer != null) { attributes.put("referrer", new ReferrerBean(referrer)); @@ -134,7 +164,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath()); break; } - attributes.put("locale", new LocaleBean(realm, locale, b, messages)); + attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle)); } attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported)); @@ -173,28 +203,47 @@ public class FreeMarkerAccountProvider implements AccountProvider { this.passwordSet = passwordSet; return this; } + + protected void setMessage(MessageType type, String message, Object... parameters) { + messageType = type; + messages = new ArrayList<>(); + messages.add(new FormMessage(null, message, parameters)); + } + + protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) { + if (message == null) + return null; + if (messagesBundle.containsKey(message.getMessage())) { + return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale) + .format(message.getParameters()); + } else { + return message.getMessage(); + } + } + + @Override + public AccountProvider setErrors(List messages) { + this.messageType = MessageType.ERROR; + this.messages = new ArrayList<>(messages); + return this; + } + @Override public AccountProvider setError(String message, Object ... parameters) { - this.message = message; - this.parameters = parameters; - this.messageType = MessageType.ERROR; + setMessage(MessageType.ERROR, message, parameters); return this; } @Override public AccountProvider setSuccess(String message, Object ... parameters) { - this.message = message; - this.parameters = parameters; - this.messageType = MessageType.SUCCESS; + setMessage(MessageType.SUCCESS, message, parameters); return this; } @Override public AccountProvider setWarning(String message, Object ... parameters) { - this.message = message; - this.parameters = parameters; - this.messageType = MessageType.WARNING; + setMessage(MessageType.WARNING, message, parameters); return this; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java deleted file mode 100644 index 6fc48be181..0000000000 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.account.freemarker.model; - -import org.keycloak.account.freemarker.FreeMarkerAccountProvider; - -/** - * @author Stian Thorgersen - */ -public class MessageBean { - - private String summary; - - private FreeMarkerAccountProvider.MessageType type; - - public MessageBean(String message, FreeMarkerAccountProvider.MessageType type) { - this.summary = message; - this.type = type; - } - - public String getSummary() { - return summary; - } - - public String getType() { - return this.type.toString().toLowerCase(); - } - - public boolean isSuccess() { - return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type); - } - - public boolean isWarning() { - return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type); - } - - public boolean isError() { - return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type); - } - -} \ No newline at end of file diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageBean.java similarity index 77% rename from forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java rename to forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageBean.java index f5e69c4145..7cfe9b8fb6 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageBean.java @@ -19,9 +19,7 @@ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ -package org.keycloak.login.freemarker.model; - -import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider; +package org.keycloak.freemarker.beans; /** * @author Stian Thorgersen @@ -30,9 +28,9 @@ public class MessageBean { private String summary; - private FreeMarkerLoginFormsProvider.MessageType type; + private MessageType type; - public MessageBean(String message, FreeMarkerLoginFormsProvider.MessageType type) { + public MessageBean(String message, MessageType type) { this.summary = message; this.type = type; } @@ -55,15 +53,15 @@ public class MessageBean { } public boolean isSuccess() { - return FreeMarkerLoginFormsProvider.MessageType.SUCCESS.equals(this.type); + return MessageType.SUCCESS.equals(this.type); } public boolean isWarning() { - return FreeMarkerLoginFormsProvider.MessageType.WARNING.equals(this.type); + return MessageType.WARNING.equals(this.type); } public boolean isError() { - return FreeMarkerLoginFormsProvider.MessageType.ERROR.equals(this.type); + return MessageType.ERROR.equals(this.type); } } \ No newline at end of file diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageType.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageType.java new file mode 100644 index 0000000000..a404c7e2ea --- /dev/null +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessageType.java @@ -0,0 +1,17 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.freemarker.beans; + +/** + * Enum with types of messages. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public enum MessageType { + + SUCCESS, WARNING, ERROR + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessagesPerFieldBean.java similarity index 90% rename from forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java rename to forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessagesPerFieldBean.java index 2384f16eb1..5858b07028 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessagesPerFieldBean.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/MessagesPerFieldBean.java @@ -3,14 +3,11 @@ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. */ -package org.keycloak.login.freemarker.model; +package org.keycloak.freemarker.beans; import java.util.HashMap; import java.util.Map; -import org.keycloak.login.FormMessage; -import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider.MessageType; - /** * Bean used to hold form messages per field. Stored under messagesPerField key in Freemarker context. * @@ -24,7 +21,7 @@ public class MessagesPerFieldBean { if (messageText == null || messageText.trim().isEmpty()) return; if (field == null) - field = FormMessage.GLOBAL; + field = "global"; MessageBean fm = messagesPerField.get(field); if (fm == null) { diff --git a/forms/common-themes/src/main/resources/theme/base/account/account.ftl b/forms/common-themes/src/main/resources/theme/base/account/account.ftl index a41d769ff5..7c349df4f7 100755 --- a/forms/common-themes/src/main/resources/theme/base/account/account.ftl +++ b/forms/common-themes/src/main/resources/theme/base/account/account.ftl @@ -14,7 +14,7 @@ -
+
@@ -24,7 +24,7 @@
-
+
*
@@ -34,7 +34,7 @@
-
+
*
@@ -44,7 +44,7 @@
-
+
*
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl index 56b5cbe5c2..a534073f0f 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -6,7 +6,7 @@ ${msg("loginProfileTitle")} <#elseif section = "form"> -
+
@@ -15,7 +15,7 @@
-
+
@@ -24,7 +24,7 @@
-
+
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index ac73087bca..c3a71d9a94 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -13,6 +13,7 @@ import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.Provider; /** 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 22fa3f7096..0dd6342adb 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,27 +1,50 @@ package org.keycloak.login.freemarker; +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + import org.jboss.logging.Logger; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.OAuth2Constants; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; -import org.keycloak.freemarker.*; +import org.keycloak.freemarker.BrowserSecurityHeaderSetup; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.LocaleHelper; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod; +import org.keycloak.freemarker.beans.LocaleBean; +import org.keycloak.freemarker.beans.MessageBean; import org.keycloak.freemarker.beans.MessageFormatterMethod; -import org.keycloak.login.FormMessage; +import org.keycloak.freemarker.beans.MessageType; +import org.keycloak.freemarker.beans.MessagesPerFieldBean; import org.keycloak.login.LoginFormsPages; import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.freemarker.model.ClientBean; import org.keycloak.login.freemarker.model.CodeBean; -import org.keycloak.freemarker.beans.LocaleBean; +import org.keycloak.login.freemarker.model.IdentityProviderBean; import org.keycloak.login.freemarker.model.LoginBean; -import org.keycloak.login.freemarker.model.MessageBean; -import org.keycloak.login.freemarker.model.MessagesPerFieldBean; import org.keycloak.login.freemarker.model.OAuthGrantBean; import org.keycloak.login.freemarker.model.ProfileBean; import org.keycloak.login.freemarker.model.RealmBean; import org.keycloak.login.freemarker.model.RegisterBean; -import org.keycloak.login.freemarker.model.IdentityProviderBean; import org.keycloak.login.freemarker.model.TotpBean; import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.models.ClientModel; @@ -30,17 +53,10 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Urls; -import javax.ws.rs.core.*; - -import java.io.IOException; -import java.net.URI; -import java.text.MessageFormat; -import java.util.*; -import java.util.concurrent.TimeUnit; - /** * @author Stian Thorgersen */ @@ -48,8 +64,6 @@ import java.util.concurrent.TimeUnit; private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); - public static enum MessageType {SUCCESS, WARNING, ERROR} - private String accessCode; private Response.Status status; private List realmRolesRequested; @@ -191,7 +205,7 @@ import java.util.concurrent.TimeUnit; if (messages != null) { MessageBean wholeMessage = new MessageBean(null, messageType); for (FormMessage message : this.messages) { - String formattedMessageText = formatMessageMessage(message, messagesBundle, locale); + String formattedMessageText = formatMessage(message, messagesBundle, locale); if (formattedMessageText != null) { wholeMessage.appendSummaryLine(formattedMessageText); messagesPerField.addMessage(message.getField(), formattedMessageText, messageType); @@ -323,7 +337,7 @@ import java.util.concurrent.TimeUnit; return null; } - protected String formatMessageMessage(FormMessage message, Properties messagesBundle, Locale locale) { + protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) { if (message == null) return null; if (messagesBundle.containsKey(message.getMessage())) { @@ -332,22 +346,27 @@ import java.util.concurrent.TimeUnit; return message.getMessage(); } } + + @Override public FreeMarkerLoginFormsProvider setError(String message, Object... parameters) { setMessage(MessageType.ERROR, message, parameters); return this; } + @Override public LoginFormsProvider setErrors(List messages) { this.messageType = MessageType.ERROR; this.messages = new ArrayList<>(messages); return this; } + @Override public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) { setMessage(MessageType.SUCCESS, message, parameters); return this; } + @Override public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) { setMessage(MessageType.WARNING, message, parameters); return this; diff --git a/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java similarity index 97% rename from forms/login-api/src/main/java/org/keycloak/login/FormMessage.java rename to model/api/src/main/java/org/keycloak/models/utils/FormMessage.java index 707ad31295..6a93c51bcd 100644 --- a/forms/login-api/src/main/java/org/keycloak/login/FormMessage.java +++ b/model/api/src/main/java/org/keycloak/models/utils/FormMessage.java @@ -3,7 +3,7 @@ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @authors tag. All rights reserved. */ -package org.keycloak.login; +package org.keycloak.models.utils; import java.util.Arrays; 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 f694661a62..fd50edaa72 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -34,6 +34,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; import org.keycloak.models.*; +import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -71,6 +72,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.Variant; + import java.lang.reflect.Method; import java.net.URI; import java.util.HashSet; @@ -405,10 +407,10 @@ public class AccountService { UserModel user = auth.getUser(); - String error = Validation.validateUpdateProfileForm(formData); - if (error != null) { + List errors = Validation.validateUpdateProfileForm(formData); + if (errors != null && !errors.isEmpty()) { setReferrerOnPage(); - return account.setError(error).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); + return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); } try { diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 0a7e8b1df4..c941cc4217 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -31,10 +31,10 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.login.FormMessage; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.*; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.LoginProtocol; @@ -618,9 +618,9 @@ public class LoginActionsService { initEvent(clientSession); - String error = Validation.validateUpdateProfileForm(formData); - if (error != null) { - return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setError(error) + List errors = Validation.validateUpdateProfileForm(formData); + if (errors != null && !errors.isEmpty()) { + return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setErrors(errors) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.UPDATE_PROFILE); } diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index 5fb98f9d96..1a4392b2a0 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -6,9 +6,9 @@ import java.util.regex.Pattern; import javax.ws.rs.core.MultivaluedMap; -import org.keycloak.login.FormMessage; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.FormMessage; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.messages.Messages; @@ -67,24 +67,24 @@ public class Validation { } - public static String validateUpdateProfileForm(MultivaluedMap formData) { + public static List validateUpdateProfileForm(MultivaluedMap formData) { + List errors = new ArrayList<>(); + if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) { - return Messages.MISSING_FIRST_NAME; + addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); } if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) { - return Messages.MISSING_LAST_NAME; + addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); } if (isEmpty(formData.getFirst(FIELD_EMAIL))) { - return Messages.MISSING_EMAIL; + addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); + } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); } - if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { - return Messages.INVALID_EMAIL; - } - - return null; + return errors; } public static boolean isEmpty(String s) {