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.List;
020import java.util.Map;
021import java.util.Optional;
022
023import org.apache.avalon.framework.parameters.Parameters;
024import org.apache.avalon.framework.service.ServiceException;
025import org.apache.avalon.framework.service.ServiceManager;
026import org.apache.cocoon.acting.ServiceableAction;
027import org.apache.cocoon.environment.ObjectModelHelper;
028import org.apache.cocoon.environment.Redirector;
029import org.apache.cocoon.environment.Request;
030import org.apache.cocoon.environment.SourceResolver;
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.core.user.CurrentUserProvider;
034import org.ametys.core.user.UserIdentity;
035import org.ametys.runtime.authentication.AuthorizationRequiredException;
036import org.ametys.runtime.i18n.I18nizableText;
037import org.ametys.web.renderingcontext.RenderingContext;
038import org.ametys.web.renderingcontext.RenderingContextHandler;
039import org.ametys.web.usermanagement.UserManagementException.StatusError;
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        boolean weakPassword = "true".equals(request.getParameter("weak-password")); // (4) when unconnected user with a weak password has to define to new password
083
084        String mode = request.getParameter("mode");
085        String login = request.getParameter("login");
086        String population = request.getParameter("population");
087        String token = request.getParameter("token");
088        
089        boolean reinitPassword = StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(login) && StringUtils.isNotEmpty(population);
090        
091        Multimap<String, I18nizableText> errors = ArrayListMultimap.create();
092                
093        try
094        {
095            if ("lostpassword".equals(mode))
096            {
097                // Unconnected user clicked on "lost password" => display the form to request a new token.
098                results.put("step", "lost-password");
099            }
100            else if (lostPassword)
101            {
102                // Unconnected user entered his email/login and population to request a new token by email
103                results.put("step", "lost-password-change");
104                resetPassword(request, siteName, language, errors);
105                if (errors.isEmpty())
106                {
107                    results.put("status", "success");
108                }
109            }
110            else if (changePassword)
111            {
112                // Connected user asks for a new token by email
113                results.put("step", "change-password-change");
114                resetConnectedUserPassword(request, siteName, language, errors);
115                if (errors.isEmpty())
116                {
117                    results.put("status", "success");
118                }
119            }
120            else if (submitPassword)
121            {
122                // Connected or unconnected user submitted a new password
123                results.put("step", "user-update");
124                changeUserPassword(request, siteName, token, errors);
125                if (errors.isEmpty())
126                {
127                    results.put("status", "success");
128                }
129            }
130            else if (reinitPassword)
131            {
132                // User clicked on email link to display the password form. 
133                // If a token is provided, check it.
134                results.put("step", "password");
135                if (weakPassword)
136                {
137                    // User with a weak password is forced to change its password. A token has been automatically generated.
138                    results.put("weak-password", "true");
139                }
140                checkPasswordToken(request, siteName, login, token, population, errors);
141                if (errors.isEmpty())
142                {
143                    results.put("status", "success");
144                }
145            }
146            else if (foUser != null)
147            {
148                // Default behavior: if user is connected, display the form to request a new token with the connected user.
149                results.put("step", "change-password");
150            }
151            else if (renderingContext == RenderingContext.FRONT)
152            {
153                // Send a 401 to force the user to authenticate.
154                throw new AuthorizationRequiredException();
155            }
156        }
157        catch (UserManagementException e)
158        {
159            errors.put("global", new I18nizableText("general-error"));
160            results.put("step", "no-form");
161            
162            getLogger().error("An error occurred resetting a user password.", e);
163        }
164        
165        request.setAttribute("errors", errors);
166        
167        return results;
168    }
169    
170    /**
171     * Reset a user's sign-up request.
172     * @param request the user request.
173     * @param siteName the site name.
174     * @param language the language.
175     * @param errors the Map to fill with errors to display to the user.
176     * @throws UserManagementException if an error occurs.
177     */
178    protected void resetPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException
179    {
180        String email = request.getParameter("email");
181        String populationId = request.getParameter("population");
182        
183        try
184        {
185            _userSignupManager.resetPassword(siteName, language, email, populationId);
186        }
187        catch (UserManagementException e)
188        {
189            StatusError statusError = e.getStatusError();
190            switch (statusError)
191            {
192                case POPULATION_UNKNOWN:
193                case USER_UNKNOWN:
194                case UNMODIFIABLE_USER_DIRECTORY:
195                case NOT_UNIQUE_USER:
196                case EMPTY_EMAIL:
197                    setGlobalError(statusError, errors, email, populationId);
198                    break;
199                default:
200                    throw e;
201            }
202        }
203    }
204    
205    /**
206     * Reset a connected user password.
207     * @param request the user request.
208     * @param siteName the site name.
209     * @param language the language.
210     * @param errors the Map to fill with errors to display to the user.
211     * @throws UserManagementException if an error occurs.
212     */
213    protected void resetConnectedUserPassword(Request request, String siteName, String language, Multimap<String, I18nizableText> errors) throws UserManagementException
214    {
215        UserIdentity foUser = _currentUserProvider.getUser();
216        
217        if (foUser == null)
218        {
219            setGlobalError(StatusError.NOT_CONNECTED, errors, null, null);
220        }
221        else
222        {
223            String login = foUser.getLogin();
224            String populationId = foUser.getPopulationId();
225            
226            try
227            {
228                _userSignupManager.resetPassword(siteName, language, login, populationId);
229            }
230            catch (UserManagementException e)
231            {
232                StatusError statusError = e.getStatusError();
233                switch (statusError)
234                {
235                    case POPULATION_UNKNOWN:
236                    case USER_UNKNOWN:
237                    case UNMODIFIABLE_USER_DIRECTORY:
238                    case NOT_UNIQUE_USER:
239                    case EMPTY_EMAIL:
240                        setGlobalError(statusError, errors, login, populationId);
241                        break;
242                    default:
243                        throw e;
244                }
245            }
246        }
247    }
248    
249    /**
250     * Check that a token is valid.
251     * @param request the user request.
252     * @param siteName the site name.
253     * @param login the user login.
254     * @param token the sign-up token that was sent to the user. 
255     * @param populationId The id of the population
256     * @param errors the Map to fill with errors to display to the user.
257     * @throws UserManagementException if an error occurs.
258     */
259    protected void checkPasswordToken(Request request, String siteName, String login, String token, String populationId, Multimap<String, I18nizableText> errors) throws UserManagementException
260    {
261        try
262        {
263            _userSignupManager.checkPasswordToken(siteName, login, token, populationId);
264        }
265        catch (UserManagementException e)
266        {
267            StatusError statusError = e.getStatusError();
268            switch (statusError)           
269            {
270                case TOKEN_UNKNOWN:
271                case TOKEN_EXPIRED:
272                    setGlobalError(statusError, errors, login, populationId);
273                    break;
274                default:
275                    throw e;
276            }
277        }
278        
279        
280    }
281    
282    /**
283     * Sign-up the user: create a real user from his temporary information.
284     * @param request the user request.
285     * @param siteName the site name.
286     * @param token the sign-up token that was sent to the user. 
287     * @param errors the Map to fill with errors to display to the user.
288     * @throws UserManagementException if an error occurs.
289     */
290    protected void changeUserPassword(Request request, String siteName, String token, Multimap<String, I18nizableText> errors) throws UserManagementException
291    {
292        // Get the login either from the request or from the connected user.
293        String login = request.getParameter("login");
294        String population = request.getParameter("population");
295        UserIdentity foUser;
296        if (StringUtils.isEmpty(login) || StringUtils.isEmpty(population))
297        {
298            foUser = _currentUserProvider.getUser();
299        }
300        else
301        {
302            foUser = new UserIdentity(login, population);
303        }
304        
305        if (foUser == null)
306        {
307            setGlobalError(StatusError.NOT_CONNECTED, errors, null, null);
308            return;
309        }
310        
311        String password = request.getParameter("password");
312        String passwordConfirmation = request.getParameter("password-confirmation");
313        
314        // First validation.
315        if (StringUtils.isBlank(password))
316        {
317            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY"));
318        }
319        else if (!password.equals(passwordConfirmation))
320        {
321            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH"));
322        }
323        
324        // Full validation.
325        List<I18nizableText> inputErrors = _userSignupManager.validatePassword(siteName, password, foUser.getLogin(), foUser.getPopulationId());
326        errors.putAll("password", inputErrors);
327        
328        if (errors.isEmpty())
329        {
330            try
331            {
332                // Validation passed: effectively change the password.
333                _userSignupManager.changeUserPassword(siteName, foUser.getLogin(), token, password, foUser.getPopulationId());
334            }
335            catch (UserManagementException e)
336            {
337                StatusError statusError = e.getStatusError();
338                switch (statusError)
339                {
340                    case TOKEN_UNKNOWN:
341                    case TOKEN_EXPIRED:
342                        setGlobalError(statusError, errors, foUser.getLogin(), foUser.getPopulationId());
343                        break;
344                    default:
345                        throw e;
346                }
347            }
348        }
349    }
350    
351    /**
352     * Set the global error if there is one.
353     * @param error The error to add (can be null)
354     * @param errors The errors map
355     * @param login The login of the user (can be null)
356     * @param populationId The population of the user (can be null)
357     */
358    protected void setGlobalError(StatusError error, Multimap<String, I18nizableText> errors, String login, String populationId)
359    {
360        if (error != null)
361        {
362            errors.put("global", new I18nizableText(error.name()));
363            if (getLogger().isWarnEnabled())
364            {
365                String message = String.format(
366                        "Error during resetting the password of the connected user '%s' of population '%s': %s",
367                        Optional.ofNullable(login).orElse(StringUtils.EMPTY),
368                        Optional.ofNullable(populationId).orElse(StringUtils.EMPTY),
369                        error.name());
370                getLogger().warn(message);
371            }
372        }
373    }
374}