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}