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.Arrays;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.apache.avalon.framework.parameters.Parameters;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.acting.ServiceableAction;
030import org.apache.cocoon.environment.ObjectModelHelper;
031import org.apache.cocoon.environment.Redirector;
032import org.apache.cocoon.environment.Request;
033import org.apache.cocoon.environment.SourceResolver;
034import org.apache.commons.lang.StringUtils;
035
036import org.ametys.core.captcha.CaptchaHelper;
037import org.ametys.core.user.UserManager;
038import org.ametys.core.user.directory.ModifiableUserDirectory;
039import org.ametys.core.user.directory.UserDirectory;
040import org.ametys.core.user.population.UserPopulation;
041import org.ametys.core.user.population.UserPopulationDAO;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.runtime.i18n.I18nizableText;
044import org.ametys.runtime.model.ModelItem;
045import org.ametys.web.URIPrefixHandler;
046import org.ametys.web.WebConstants;
047import org.ametys.web.cache.PageHelper;
048import org.ametys.web.renderingcontext.RenderingContextHandler;
049import org.ametys.web.repository.page.Page;
050import org.ametys.web.repository.page.ZoneItem;
051import org.ametys.web.site.SiteConfigurationExtensionPoint;
052import org.ametys.web.usermanagement.UserManagementException.StatusError;
053
054import com.google.common.collect.ArrayListMultimap;
055import com.google.common.collect.Multimap;
056
057/**
058 * Handle the user sign-up actions.
059 */
060public class UserSignupAction extends ServiceableAction
061{
062    /** The UsersManager standard fields. */
063    public static final Set<String> STANDARD_FIELDS = new HashSet<>(Arrays.asList("login", "firstname", "lastname", "email", "address", "password"));
064    
065    /** The user signup manager. */
066    protected UserSignupManager _userSignupManager;
067    
068    /** The user manager. */
069    protected UserManager _userManager;
070    
071    /** The DAO for user populations */
072    protected UserPopulationDAO _userPopulationDAO;
073    /** The site configuration EP. */
074    protected SiteConfigurationExtensionPoint _siteConfiguration;
075    
076    /** The Ametys Resolver */
077    protected AmetysObjectResolver _resolver;
078    
079    /** Page helper */
080    protected PageHelper _pageHelper;
081    /** The rendering context */
082    protected RenderingContextHandler _renderingContextHandler;
083    /** The URI prefix handler */
084    protected URIPrefixHandler _prefixHandler;
085
086    @Override
087    public void service(ServiceManager serviceManager) throws ServiceException
088    {
089        super.service(serviceManager);
090        _userSignupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE);
091        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
092        _userPopulationDAO = (UserPopulationDAO) serviceManager.lookup(UserPopulationDAO.ROLE);
093        _siteConfiguration = (SiteConfigurationExtensionPoint) serviceManager.lookup(SiteConfigurationExtensionPoint.ROLE);
094        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
095        _pageHelper = (PageHelper) serviceManager.lookup(PageHelper.ROLE);
096        _renderingContextHandler = (RenderingContextHandler) serviceManager.lookup(RenderingContextHandler.ROLE);
097        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
098    }
099    
100    @Override
101    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
102    {
103        Request request = ObjectModelHelper.getRequest(objectModel);
104        String siteName = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITE_NAME);
105        String language = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
106        
107        Map<String, String> results = new HashMap<>();
108        
109        String signup = request.getParameter("signup");
110        String submitPassword = request.getParameter("pwd-submit");
111        String mode = request.getParameter("mode");
112        String email = request.getParameter("email");
113        String firstname = request.getParameter("firstname");
114        String lastname = request.getParameter("lastname");
115        String token = request.getParameter("token");
116        
117        Multimap<String, I18nizableText> errors = ArrayListMultimap.create();
118        
119        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
120        
121        try
122        {
123            UserDirectory userDirectory = _userSignupManager.getUserDirectory(zoneItem);
124            String populationId = userDirectory.getPopulationId();
125            String userDirectoryId = userDirectory.getId();
126            
127            if ("new-token".equals(mode))
128            {
129                // Action which requests a new token.
130                results.put("step", "new-token");
131                resetTempSignup(request, siteName, language, populationId, userDirectoryId, errors);
132                if (errors.isEmpty())
133                {
134                    results.put("status", "success");
135                }
136            }
137            else if ("true".equals(submitPassword))
138            {
139                // If the password has been entered, signup the user.
140                results.put("step", "signup");
141                doSignup(request, parameters, siteName, language, firstname, lastname, email, token, populationId, userDirectoryId, errors);
142                if (errors.isEmpty())
143                {
144                    results.put("status", "success");
145                }
146                else 
147                {
148                    results.put("step", "password");
149                }
150            }
151            else if (StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(email))
152            {
153                // If a token is provided, check it.
154                results.put("step", "password");
155                checkToken(request, siteName, email, token, populationId, userDirectoryId, errors);
156                if (errors.isEmpty())
157                {
158                    results.put("status", "success");
159                }
160            }
161            else if ("true".equals(signup))
162            {
163                // A signup request has been made.
164                results.put("step", "temp-signup");
165                temporarySignup(request, parameters, siteName, language, populationId, userDirectoryId, errors);
166                if (errors.isEmpty())
167                {
168                    results.put("status", "success");
169                }
170            }
171            else if (!_userSignupManager.isPublicSignupAllowed(siteName))
172            {
173                // Can not access to signup form (first step) if public signup is unauthorized 
174                errors.put("global", new I18nizableText("error-unauthorized"));
175            }
176        }
177        catch (Exception e)
178        {
179            errors.put("global", new I18nizableText("general-error"));
180            
181            getLogger().error("An error occurred signing a user up.", e);
182        }
183        
184        request.setAttribute("errors", errors);
185        
186        return results;
187    }
188    
189    /**
190     * Store a user's sign-up request.
191     * @param request the user request.
192     * @param parameters the action parameters.
193     * @param siteName the site name.
194     * @param language the language.
195     * @param populationId The id of the population
196     * @param userDirectoryId The id of the user directory of the population
197     * @param errors the Map to fill with errors to display to the user.
198     * @throws UserManagementException if an error occurs.
199     */
200    protected void temporarySignup(Request request, Parameters parameters, String siteName, String language, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
201    {
202        String email = request.getParameter("email");
203        
204        Page page = _resolver.resolveById(request.getParameter("page-id"));
205        
206        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
207        if (userPopulation == null)
208        {
209            throw new IllegalStateException("The signup service in page '" + page.getTitle() + " (" + page.getId() + ")' is configured with the unexisting '" + populationId + "' user population identifier. Please reconfigure the service.");
210        }
211        ModifiableUserDirectory userDirectory = (ModifiableUserDirectory) userPopulation.getUserDirectory(userDirectoryId);
212        if (userDirectory == null)
213        {
214            throw new IllegalStateException("The signup service in page '" + page.getTitle() + " (" + page.getId() + ")' is configured with the unexisting '" + userDirectoryId + "' user directory identifier. Please reconfigure the service.");
215        }
216        
217        Map<String, String> additionalFields = getAdditionalValues(request, userDirectory);
218        
219        if (_pageHelper.isCaptchaRequired(page))
220        {   
221            String captchaValue = request.getParameter("captcha");
222            String captchaKey = request.getParameter("captcha-key");
223
224            // Validate the input
225            if (!CaptchaHelper.checkAndInvalidate(captchaKey, captchaValue))
226            {
227                errors.put("captcha", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_FORM_ERROR_INVALID_CAPTCHA"));
228            }
229        }
230        
231        
232        Map<String, List<I18nizableText>> inputErrors = _userSignupManager.validate(siteName, email, additionalFields);
233        
234        for (String field : inputErrors.keySet())
235        {
236            errors.putAll(field, inputErrors.get(field));
237        }
238        
239        if (errors.isEmpty())
240        {
241            try
242            {
243                _userSignupManager.temporarySignup(siteName, language, email, populationId, userDirectoryId);
244            }
245            catch (UserManagementException e)
246            {
247                switch (e.getStatusError())
248                {
249                    case USER_ALREADY_EXISTS:
250                        errors.put("global", new I18nizableText("user-email-already-exists"));
251                        break;
252                    case TEMP_USER_ALREADY_EXISTS:
253                        errors.put("global", new I18nizableText("temp-email-already-exists"));
254                        break;
255                    default:
256                        throw e;
257                }
258                        
259            }
260        }
261    }
262    
263    /**
264     * Reset a user's sign-up request.
265     * @param request the user request.
266     * @param siteName the site name.
267     * @param language the language.
268     * @param populationId The id of the population
269     * @param userDirectoryId The id of the user directory of the population
270     * @param errors the Map to fill with errors to display to the user.
271     * @throws UserManagementException if an error occurs.
272     */
273    protected void resetTempSignup(Request request, String siteName, String language, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
274    {
275        String email = request.getParameter("email");
276        
277        try
278        {
279            _userSignupManager.resetTempSignup(siteName, language, email, populationId, userDirectoryId);
280        }
281        catch (UserManagementException e)
282        {
283            switch (e.getStatusError())
284            {
285                case UNKNOWN_EMAIL:
286                    errors.put("global", new I18nizableText("temp-email-unknown"));
287                    break;
288                default:
289                    throw e;
290            }
291        }
292    }
293    
294    /**
295     * Check that a token is valid.
296     * @param request the user request.
297     * @param siteName the site name.
298     * @param email the user e-mail.
299     * @param token the sign-up token that was sent to the user. 
300     * @param populationId The id of the population
301     * @param userDirectoryId The id of the user directory of the population
302     * @param errors the Map to fill with errors to display to the user.
303     * @throws UserManagementException if an error occurs.
304     */
305    protected void checkToken(Request request, String siteName, String email, String token, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
306    {
307        try
308        {
309            _userSignupManager.checkToken(siteName, email, token, populationId, userDirectoryId);
310        }
311        catch (UserManagementException e)
312        {
313            switch (e.getStatusError())
314            {
315                case TOKEN_UNKNOWN:
316                    errors.put("global", new I18nizableText("error-token-unknown"));
317                    break;
318                case TOKEN_EXPIRED:
319                    errors.put("global", new I18nizableText("error-token-expired"));
320                    break;
321                default:
322                    throw e;
323            }
324        }
325    }
326    
327    /**
328     * Sign-up the user: create a real user from his temporary information.
329     * @param request the user request.
330     * @param parameters the action parameters.
331     * @param siteName the site name.
332     * @param language the current language.
333     * @param firstname the user firstname.
334     * @param lastname the user lastname.
335     * @param email the user e-mail.
336     * @param token the sign-up token that was sent to the user. 
337     * @param populationId The id of the population
338     * @param userDirectoryId The id of the user directory of the population
339     * @param errors the Map to fill with errors to display to the user.
340     * @throws UserManagementException if an error occurs.
341     */
342    protected void doSignup(Request request, Parameters parameters, String siteName, String language, String firstname, String lastname, String email, String token, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
343    {
344        String password = request.getParameter("password");
345        String passwordConfirmation = request.getParameter("password-confirmation");
346        String tos = request.getParameter("tos");
347        String tosMode = parameters.getParameter("tos-mode", "");
348        
349        if (StringUtils.isBlank(password))
350        {
351            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY"));
352        }
353        else if (!password.equals(passwordConfirmation))
354        {
355            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH"));
356        }
357        
358        if (StringUtils.isBlank(firstname))
359        {
360            errors.put("firstname", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_FIRSTNAME_EMPTY"));
361        }
362        
363        if (StringUtils.isBlank(lastname))
364        {
365            errors.put("lastname", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_LASTNAME_EMPTY"));
366        }
367        
368        if (!"NONE".equals(tosMode) && !"true".equals(tos))
369        {
370            errors.put("tos", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_FORM_ERROR_TOS_NOT_CHECKED"));
371        }
372        
373        UserDirectory userDirectory = _userPopulationDAO.getUserPopulation(populationId).getUserDirectory(userDirectoryId);
374        if (!(userDirectory instanceof ModifiableUserDirectory modifiableUserDirectory))
375        {
376            throw new UserManagementException("The user subscription feature can't be used, as the UserDirectory is not modifiable.", StatusError.UNMODIFIABLE_USER_DIRECTORY);
377        }
378        
379        // Standard info for user.
380        Map<String, String> userInfos = new HashMap<>();
381        userInfos.put("email", email);
382        userInfos.put("firstname", firstname);
383        userInfos.put("lastname", lastname);
384        userInfos.put("password", password);
385        
386        Map<String, List<I18nizableText>> inputErrors = modifiableUserDirectory.validate(userInfos);
387        inputErrors.remove("login"); // ignore errors on login (user do not have login yet)
388        for (String field : inputErrors.keySet())
389        {
390            errors.putAll(field, inputErrors.get(field));
391        }
392        
393        if (errors.isEmpty())
394        {
395            try
396            {
397                _userSignupManager.signup(siteName, language, firstname, lastname, email, token, password, populationId, userDirectoryId, errors);
398            }
399            catch (UserManagementException e)
400            {
401                switch (e.getStatusError())               
402                {
403                    case TOKEN_UNKNOWN:
404                        errors.put("global", new I18nizableText("error-token-unknown"));
405                        break;
406                    case TOKEN_EXPIRED:
407                        errors.put("global", new I18nizableText("error-token-expired"));
408                        break;
409                    default:
410                        throw e;
411                }
412            }
413        }
414    }
415       
416    /**
417     * Get FO user manager's custom field values.
418     * @param request the request.
419     * @param userDirectory the user directory
420     * @return the custom field values.
421     * @throws UserManagementException if an error occurs.
422     */
423    protected Map<String, String> getAdditionalValues(Request request, ModifiableUserDirectory userDirectory) throws UserManagementException
424    {
425        Map<String, String> additionalFields = new HashMap<>();
426        
427        Collection< ? extends ModelItem> fields = userDirectory.getModelItems();
428        
429        for (ModelItem field : fields)
430        {
431            String fieldId = field.getName();
432            if (!STANDARD_FIELDS.contains(fieldId))
433            {
434                String value = request.getParameter(fieldId);
435                
436                additionalFields.put(fieldId, value);
437            }
438        }
439        
440        return additionalFields;
441    }
442}