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        switch (status)
178        {
179            case UserSignupManager.LOST_PASSWORD_USER_UNKNOWN:
180                errors.put("global", new I18nizableText("user-unknown"));
181                break;
182            case UserSignupManager.LOST_PASSWORD_POPULATION_UNKNOWN:
183                errors.put("global", new I18nizableText("population-unknown"));
184                break;
185            case UserSignupManager.LOST_PASSWORD_UNMODIFIABLE_USER_DIRECTORY:
186                errors.put("global", new I18nizableText("unmodifiable-user-directory"));
187                break;
188            case UserSignupManager.LOST_PASSWORD_EMPTY_EMAIL:
189                errors.put("global", new I18nizableText("empty-email"));
190                break;
191            default:
192                // Nothing to do
193        }
194    }
195    
196    /**
197     * Reset a connected user password.
198     * @param request the user request.
199     * @param siteName the site name.
200     * @param language the language.
201     * @param errors the Map to fill with errors to display to the user.
202     * @throws UserManagementException if an error occurs.
203     */
204    protected void resetConnectedUserPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException
205    {
206        UserIdentity foUser = _currentUserProvider.getUser();
207        String login;
208        String populationId;
209        
210        if (foUser == null)
211        {
212            errors.put("global", new I18nizableText("not-connected"));
213            login = null;
214            populationId = null;
215        }
216        else
217        {
218            login = foUser.getLogin();
219            populationId = foUser.getPopulationId();
220        }
221        
222        int status = _userSignupManager.resetPassword(siteName, language, login, populationId);
223
224        switch (status)
225        {
226            case UserSignupManager.LOST_PASSWORD_USER_UNKNOWN:
227                errors.put("global", new I18nizableText("user-unknown"));
228                break;
229            case UserSignupManager.LOST_PASSWORD_POPULATION_UNKNOWN:
230                errors.put("global", new I18nizableText("population-unknown"));
231                break;
232            case UserSignupManager.LOST_PASSWORD_UNMODIFIABLE_USER_DIRECTORY:
233                errors.put("global", new I18nizableText("unmodifiable-user-directory"));
234                break;
235            case UserSignupManager.LOST_PASSWORD_EMPTY_EMAIL:
236                errors.put("global", new I18nizableText("empty-email"));
237                break;
238            default:
239                // Nothing to do
240        }
241    }
242    
243    /**
244     * Check that a token is valid.
245     * @param request the user request.
246     * @param siteName the site name.
247     * @param login the user login.
248     * @param token the sign-up token that was sent to the user. 
249     * @param populationId The id of the population
250     * @param errors the Map to fill with errors to display to the user.
251     * @throws UserManagementException if an error occurs.
252     */
253    protected void checkPasswordToken(Request request, String siteName, String login, String token, String populationId, Multimap<String, I18nizableText> errors) throws UserManagementException
254    {
255        int tokenStatus = _userSignupManager.checkPasswordToken(siteName, login, token, populationId);
256        
257        if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_UNKNOWN)
258        {
259            errors.put("global", new I18nizableText("error-token-unknown"));
260        }
261        else if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_EXPIRED)
262        {
263            errors.put("global", new I18nizableText("error-token-expired"));
264        }
265    }
266    
267    /**
268     * Sign-up the user: create a real user from his temporary information.
269     * @param request the user request.
270     * @param siteName the site name.
271     * @param token the sign-up token that was sent to the user. 
272     * @param errors the Map to fill with errors to display to the user.
273     * @throws UserManagementException if an error occurs.
274     */
275    protected void changeUserPassword(Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException
276    {
277        // Get the login either from the request or from the connected user.
278        String login = request.getParameter("login");
279        String population = request.getParameter("population");
280        UserIdentity foUser;
281        if (StringUtils.isEmpty(login) || StringUtils.isEmpty(population))
282        {
283            foUser = _currentUserProvider.getUser();
284        }
285        else
286        {
287            foUser = new UserIdentity(login, population);
288        }
289        
290        if (foUser == null)
291        {
292            errors.put("global", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_NO_USER"));
293            return;
294        }
295        
296        String password = request.getParameter("password");
297        String passwordConfirmation = request.getParameter("password-confirmation");
298        
299        // First validation.
300        if (StringUtils.isBlank(password))
301        {
302            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY"));
303        }
304        else if (!password.equals(passwordConfirmation))
305        {
306            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH"));
307        }
308        
309        // Full validation.
310        Map<String, Errors> inputErrors = _userSignupManager.validatePassword(siteName, password, foUser.getLogin(), foUser.getPopulationId());
311        for (String field : inputErrors.keySet())
312        {
313            errors.putAll(field, inputErrors.get(field).getErrors());
314        }
315        
316        if (errors.isEmpty())
317        {
318            // Validation passed: effectively change the password.
319            int status = _userSignupManager.changeUserPassword(siteName, foUser.getLogin(), token, password, foUser.getPopulationId());
320            
321            if (status == UserSignupManager.SIGNUP_TOKEN_UNKNOWN)
322            {
323                errors.put("global", new I18nizableText("error-token-unknown"));
324            }
325            else if (status == UserSignupManager.SIGNUP_TOKEN_EXPIRED)
326            {
327                errors.put("global", new I18nizableText("error-token-expired"));
328            }
329        }
330    }
331    
332}