001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.web.usermanagement; 017 018import java.util.HashMap; 019import java.util.Map; 020import java.util.Optional; 021 022import org.apache.avalon.framework.parameters.Parameters; 023import org.apache.avalon.framework.service.ServiceException; 024import org.apache.avalon.framework.service.ServiceManager; 025import org.apache.cocoon.acting.ServiceableAction; 026import org.apache.cocoon.environment.ObjectModelHelper; 027import org.apache.cocoon.environment.Redirector; 028import org.apache.cocoon.environment.Request; 029import org.apache.cocoon.environment.SourceResolver; 030import org.apache.commons.lang.StringUtils; 031 032import org.ametys.core.user.CurrentUserProvider; 033import org.ametys.core.user.UserIdentity; 034import org.ametys.runtime.authentication.AuthorizationRequiredException; 035import org.ametys.runtime.i18n.I18nizableText; 036import org.ametys.runtime.parameter.Errors; 037import org.ametys.web.renderingcontext.RenderingContext; 038import org.ametys.web.renderingcontext.RenderingContextHandler; 039import org.ametys.web.usermanagement.UserSignupManager.LostPasswordError; 040 041import com.google.common.collect.ArrayListMultimap; 042import com.google.common.collect.Multimap; 043 044/** 045 * Handle the lost password and change password actions. 046 */ 047public class UserPasswordAction extends ServiceableAction 048{ 049 /** The user signup manager. */ 050 protected UserSignupManager _userSignupManager; 051 052 /** The rendering context handler. */ 053 protected RenderingContextHandler _renderingContextHandler; 054 055 /** The current user provider */ 056 protected CurrentUserProvider _currentUserProvider; 057 058 @Override 059 public void service(ServiceManager serviceManager) throws ServiceException 060 { 061 super.service(serviceManager); 062 _userSignupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE); 063 _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE); 064 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 065 } 066 067 @Override 068 public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 069 { 070 Request request = ObjectModelHelper.getRequest(objectModel); 071 String siteName = (String) request.getAttribute("site"); 072 String language = (String) request.getAttribute("sitemapLanguage"); 073 RenderingContext renderingContext = _renderingContextHandler.getRenderingContext(); 074 075 Map<String, String> results = new HashMap<>(); 076 077 UserIdentity foUser = _currentUserProvider.getUser(); 078 079 boolean lostPassword = "true".equals(request.getParameter("lost-password")); // (1) when unconnected user ask for a new password 080 boolean changePassword = "true".equals(request.getParameter("change-password")); // (2) when connected user ask for change password 081 boolean submitPassword = "true".equals(request.getParameter("pwd-submit")); // (3) when connected or unconnected user submit a new password 082 083 String mode = request.getParameter("mode"); 084 String login = request.getParameter("login"); 085 String population = request.getParameter("population"); 086 String token = request.getParameter("token"); 087 088 boolean reinitPassword = StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(login) && StringUtils.isNotEmpty(population); 089 090 Multimap<String, I18nizableText> errors = ArrayListMultimap.create(); 091 092 try 093 { 094 if ("lostpassword".equals(mode)) 095 { 096 // Unconnected user clicked on "lost password" => display the form to request a new token. 097 results.put("step", "lost-password"); 098 } 099 else if (lostPassword) 100 { 101 // Unconnected user entered his email/login and population to request a new token by email 102 results.put("step", "lost-password-change"); 103 resetPassword(request, siteName, language, errors); 104 if (errors.isEmpty()) 105 { 106 results.put("status", "success"); 107 } 108 } 109 else if (changePassword) 110 { 111 // Connected user asks for a new token by email 112 results.put("step", "change-password-change"); 113 resetConnectedUserPassword(request, siteName, language, errors); 114 if (errors.isEmpty()) 115 { 116 results.put("status", "success"); 117 } 118 } 119 else if (submitPassword) 120 { 121 // Connected or unconnected user submitted a new password 122 results.put("step", "user-update"); 123 changeUserPassword(request, siteName, token, errors); 124 if (errors.isEmpty()) 125 { 126 results.put("status", "success"); 127 } 128 } 129 else if (reinitPassword) 130 { 131 // User clicked on email link to display the password form. 132 // If a token is provided, check it. 133 results.put("step", "password"); 134 checkPasswordToken(request, siteName, login, token, population, errors); 135 if (errors.isEmpty()) 136 { 137 results.put("status", "success"); 138 } 139 } 140 else if (foUser != null) 141 { 142 // Default behavior: if user is connected, display the form to request a new token with the connected user. 143 results.put("step", "change-password"); 144 } 145 else if (renderingContext == RenderingContext.FRONT) 146 { 147 // Send a 401 to force the user to authenticate. 148 throw new AuthorizationRequiredException(); 149 } 150 } 151 catch (UserManagementException e) 152 { 153 errors.put("global", new I18nizableText("general-error")); 154 results.put("step", "no-form"); 155 156 getLogger().error("An error occurred resetting a user password.", e); 157 } 158 159 request.setAttribute("errors", errors); 160 161 return results; 162 } 163 164 /** 165 * Reset a user's sign-up request. 166 * @param request the user request. 167 * @param siteName the site name. 168 * @param language the language. 169 * @param errors the Map to fill with errors to display to the user. 170 * @throws UserManagementException if an error occurs. 171 */ 172 protected void resetPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 173 { 174 String email = request.getParameter("email"); 175 String populationId = request.getParameter("population"); 176 177 LostPasswordError error = _userSignupManager.resetPassword(siteName, language, email, populationId); 178 setGlobalError(error, errors, email, populationId); 179 } 180 181 /** 182 * Reset a connected user password. 183 * @param request the user request. 184 * @param siteName the site name. 185 * @param language the language. 186 * @param errors the Map to fill with errors to display to the user. 187 * @throws UserManagementException if an error occurs. 188 */ 189 protected void resetConnectedUserPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException 190 { 191 UserIdentity foUser = _currentUserProvider.getUser(); 192 193 if (foUser == null) 194 { 195 setGlobalError(LostPasswordError.NOT_CONNECTED, errors, null, null); 196 } 197 else 198 { 199 String login = foUser.getLogin(); 200 String populationId = foUser.getPopulationId(); 201 202 LostPasswordError error = _userSignupManager.resetPassword(siteName, language, foUser.getLogin(), foUser.getPopulationId()); 203 setGlobalError(error, errors, login, populationId); 204 } 205 } 206 207 /** 208 * Check that a token is valid. 209 * @param request the user request. 210 * @param siteName the site name. 211 * @param login the user login. 212 * @param token the sign-up token that was sent to the user. 213 * @param populationId The id of the population 214 * @param errors the Map to fill with errors to display to the user. 215 * @throws UserManagementException if an error occurs. 216 */ 217 protected void checkPasswordToken(Request request, String siteName, String login, String token, String populationId, Multimap<String, I18nizableText> errors) throws UserManagementException 218 { 219 int tokenStatus = _userSignupManager.checkPasswordToken(siteName, login, token, populationId); 220 221 LostPasswordError error = null; 222 if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_UNKNOWN) 223 { 224 error = LostPasswordError.TOKEN_UNKNOWN; 225 } 226 else if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_EXPIRED) 227 { 228 error = LostPasswordError.TOKEN_EXPIRED; 229 } 230 setGlobalError(error, errors, login, populationId); 231 } 232 233 /** 234 * Sign-up the user: create a real user from his temporary information. 235 * @param request the user request. 236 * @param siteName the site name. 237 * @param token the sign-up token that was sent to the user. 238 * @param errors the Map to fill with errors to display to the user. 239 * @throws UserManagementException if an error occurs. 240 */ 241 protected void changeUserPassword(Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException 242 { 243 // Get the login either from the request or from the connected user. 244 String login = request.getParameter("login"); 245 String population = request.getParameter("population"); 246 UserIdentity foUser; 247 if (StringUtils.isEmpty(login) || StringUtils.isEmpty(population)) 248 { 249 foUser = _currentUserProvider.getUser(); 250 } 251 else 252 { 253 foUser = new UserIdentity(login, population); 254 } 255 256 if (foUser == null) 257 { 258 setGlobalError(LostPasswordError.NOT_CONNECTED, errors, null, null); 259 return; 260 } 261 262 String password = request.getParameter("password"); 263 String passwordConfirmation = request.getParameter("password-confirmation"); 264 265 // First validation. 266 if (StringUtils.isBlank(password)) 267 { 268 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY")); 269 } 270 else if (!password.equals(passwordConfirmation)) 271 { 272 errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH")); 273 } 274 275 // Full validation. 276 Map<String, Errors> inputErrors = _userSignupManager.validatePassword(siteName, password, foUser.getLogin(), foUser.getPopulationId()); 277 for (String field : inputErrors.keySet()) 278 { 279 errors.putAll(field, inputErrors.get(field).getErrors()); 280 } 281 282 if (errors.isEmpty()) 283 { 284 // Validation passed: effectively change the password. 285 int status = _userSignupManager.changeUserPassword(siteName, foUser.getLogin(), token, password, foUser.getPopulationId()); 286 287 LostPasswordError error = null; 288 if (status == UserSignupManager.SIGNUP_TOKEN_UNKNOWN) 289 { 290 error = LostPasswordError.TOKEN_UNKNOWN; 291 } 292 else if (status == UserSignupManager.SIGNUP_TOKEN_EXPIRED) 293 { 294 error = LostPasswordError.TOKEN_EXPIRED; 295 } 296 setGlobalError(error, errors, foUser.getLogin(), foUser.getPopulationId()); 297 } 298 } 299 300 /** 301 * Set the global error if there is one. 302 * @param error The error to add (can be null) 303 * @param errors The errors map 304 * @param login The login of the user (can be null) 305 * @param populationId The population of the user (can be null) 306 */ 307 protected void setGlobalError(LostPasswordError error, Multimap<String, I18nizableText> errors, String login, String populationId) 308 { 309 if (error != null) 310 { 311 errors.put("global", new I18nizableText(error.name())); 312 if (getLogger().isWarnEnabled()) 313 { 314 String message = String.format( 315 "Error during resetting the password of the connected user '%s' of population '%s': %s", 316 Optional.ofNullable(login).orElse(StringUtils.EMPTY), 317 Optional.ofNullable(populationId).orElse(StringUtils.EMPTY), 318 error.name()); 319 getLogger().warn(message); 320 } 321 } 322 } 323}