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