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.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.parameters.Parameters;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.cocoon.acting.ServiceableAction;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Redirector;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.environment.SourceResolver;
033import org.apache.commons.lang.StringUtils;
034
035import org.ametys.core.captcha.CaptchaHelper;
036import org.ametys.core.user.UserManager;
037import org.ametys.core.user.directory.ModifiableUserDirectory;
038import org.ametys.core.user.population.UserPopulation;
039import org.ametys.core.user.population.UserPopulationDAO;
040import org.ametys.plugins.repository.AmetysObjectResolver;
041import org.ametys.runtime.i18n.I18nizableText;
042import org.ametys.runtime.model.ModelItem;
043import org.ametys.runtime.parameter.Errors;
044import org.ametys.web.URIPrefixHandler;
045import org.ametys.web.WebConstants;
046import org.ametys.web.cache.PageHelper;
047import org.ametys.web.renderingcontext.RenderingContextHandler;
048import org.ametys.web.repository.page.Page;
049import org.ametys.web.repository.page.ZoneItem;
050import org.ametys.web.site.SiteConfigurationExtensionPoint;
051
052import com.google.common.collect.ArrayListMultimap;
053import com.google.common.collect.Multimap;
054
055/**
056 * Handle the user sign-up actions.
057 */
058public class UserSignupAction extends ServiceableAction
059{
060    /** The UsersManager standard fields. */
061    public static final Set<String> STANDARD_FIELDS = new HashSet<>(Arrays.asList("login", "firstname", "lastname", "email", "address", "password"));
062    
063    private static final String __SIGNUP_SERVICE_PARAMETER_USERDIRECTORY = "userdirectory";
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        String userDirectoryParameterValue = zoneItem.getServiceParameters().getValue(__SIGNUP_SERVICE_PARAMETER_USERDIRECTORY);
121        String[] populationAndDirectory = userDirectoryParameterValue.split("#", 2);
122        String populationId = populationAndDirectory[0];
123        String userDirectoryId = populationAndDirectory[1];
124        
125        try
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        }
172        catch (Exception e)
173        {
174            errors.put("global", new I18nizableText("general-error"));
175            
176            getLogger().error("An error occurred signing a user up.", e);
177        }
178        
179        request.setAttribute("errors", errors);
180        
181        return results;
182    }
183    
184    /**
185     * Store a user's sign-up request.
186     * @param request the user request.
187     * @param parameters the action parameters.
188     * @param siteName the site name.
189     * @param language the language.
190     * @param populationId The id of the population
191     * @param userDirectoryId The id of the user directory of the population
192     * @param errors the Map to fill with errors to display to the user.
193     * @throws UserManagementException if an error occurs.
194     */
195    protected void temporarySignup(Request request, Parameters parameters, String siteName, String language, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
196    {
197        String email = request.getParameter("email");
198        
199        Page page = _resolver.resolveById(request.getParameter("page-id"));
200        
201        UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(populationId);
202        if (userPopulation == null)
203        {
204            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.");
205        }
206        ModifiableUserDirectory userDirectory = (ModifiableUserDirectory) userPopulation.getUserDirectory(userDirectoryId);
207        if (userDirectory == null)
208        {
209            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.");
210        }
211        
212        Map<String, String> additionalFields = getAdditionalValues(request, userDirectory);
213        
214        if (_pageHelper.isCaptchaRequired(page))
215        {   
216            String captchaValue = request.getParameter("captcha");
217            String captchaKey = request.getParameter("captcha-key");
218
219            // Validate the input
220            if (!CaptchaHelper.checkAndInvalidate(captchaKey, captchaValue))
221            {
222                errors.put("captcha", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_FORM_ERROR_INVALID_CAPTCHA"));
223            }
224        }
225        
226        if (_userSignupManager.userExists(email, siteName))
227        {
228            errors.put("global", new I18nizableText("user-email-already-exists"));
229        }
230        
231        Map<String, Errors> inputErrors = _userSignupManager.validate(siteName, email, additionalFields);
232        
233        for (String field : inputErrors.keySet())
234        {
235            errors.putAll(field, inputErrors.get(field).getErrors());
236        }
237        
238        if (errors.isEmpty())
239        {
240            int status = _userSignupManager.temporarySignup(siteName, language, email, populationId, userDirectoryId);
241            
242            if (status == UserSignupManager.SIGNUP_ERROR_TEMP_EMAIL_ALREADY_EXISTS)
243            {
244                errors.put("global", new I18nizableText("temp-email-already-exists"));
245            }
246        }
247    }
248    
249    /**
250     * Reset a user's sign-up request.
251     * @param request the user request.
252     * @param siteName the site name.
253     * @param language the language.
254     * @param populationId The id of the population
255     * @param userDirectoryId The id of the user directory 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 resetTempSignup(Request request, String siteName, String language, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
260    {
261        String email = request.getParameter("email");
262        
263        int status = _userSignupManager.resetTempSignup(siteName, language, email, populationId, userDirectoryId);
264        
265        if (status == UserSignupManager.SIGNUP_RESET_ERROR_EMAIL_UNKNOWN)
266        {
267            errors.put("global", new I18nizableText("temp-email-unknown"));
268        }
269    }
270    
271    /**
272     * Check that a token is valid.
273     * @param request the user request.
274     * @param siteName the site name.
275     * @param email the user e-mail.
276     * @param token the sign-up token that was sent to the user. 
277     * @param populationId The id of the population
278     * @param userDirectoryId The id of the user directory of the population
279     * @param errors the Map to fill with errors to display to the user.
280     * @throws UserManagementException if an error occurs.
281     */
282    protected void checkToken(Request request, String siteName, String email, String token, String populationId, String userDirectoryId, Multimap<String, I18nizableText> errors) throws UserManagementException
283    {
284        int tokenStatus = _userSignupManager.checkToken(siteName, email, token, populationId, userDirectoryId);
285        
286        if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_UNKNOWN)
287        {
288            errors.put("global", new I18nizableText("error-token-unknown"));
289        }
290        else if (tokenStatus == UserSignupManager.SIGNUP_TOKEN_EXPIRED)
291        {
292            errors.put("global", new I18nizableText("error-token-expired"));
293        }
294    }
295    
296    /**
297     * Sign-up the user: create a real user from his temporary information.
298     * @param request the user request.
299     * @param parameters the action parameters.
300     * @param siteName the site name.
301     * @param language the current language.
302     * @param firstname the user firstname.
303     * @param lastname the user lastname.
304     * @param email the user e-mail.
305     * @param token the sign-up token that was sent to the user. 
306     * @param populationId The id of the population
307     * @param userDirectoryId The id of the user directory of the population
308     * @param errors the Map to fill with errors to display to the user.
309     * @throws UserManagementException if an error occurs.
310     */
311    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
312    {
313        String password = request.getParameter("password");
314        String passwordConfirmation = request.getParameter("password-confirmation");
315        String tos = request.getParameter("tos");
316        String tosMode = parameters.getParameter("tos-mode", "");
317        
318        if (StringUtils.isBlank(password))
319        {
320            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_EMPTY"));
321        }
322        else if (!password.equals(passwordConfirmation))
323        {
324            errors.put("password", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_PASSWORD_CONFIRMATION_DOESNT_MATCH"));
325        }
326        
327        if (StringUtils.isBlank(firstname))
328        {
329            errors.put("firstname", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_FIRSTNAME_EMPTY"));
330        }
331        
332        if (StringUtils.isBlank(lastname))
333        {
334            errors.put("lastname", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_ERROR_LASTNAME_EMPTY"));
335        }
336        
337        if (!"NONE".equals(tosMode) && !"true".equals(tos))
338        {
339            errors.put("tos", new I18nizableText("plugin.web", "PLUGINS_WEB_USER_SIGNUP_FORM_ERROR_TOS_NOT_CHECKED"));
340        }
341        
342        if (errors.isEmpty())
343        {
344            int status = _userSignupManager.signup(siteName, language, firstname, lastname, email, token, password, populationId, userDirectoryId, errors);
345            
346            if (status == UserSignupManager.SIGNUP_TOKEN_UNKNOWN)
347            {
348                errors.put("global", new I18nizableText("error-token-unknown"));
349            }
350            else if (status == UserSignupManager.SIGNUP_TOKEN_EXPIRED)
351            {
352                errors.put("global", new I18nizableText("error-token-expired"));
353            }
354        }
355    }
356       
357    /**
358     * Get FO user manager's custom field values.
359     * @param request the request.
360     * @param userDirectory the user directory
361     * @return the custom field values.
362     * @throws UserManagementException if an error occurs.
363     */
364    protected Map<String, String> getAdditionalValues(Request request, ModifiableUserDirectory userDirectory) throws UserManagementException
365    {
366        Map<String, String> additionalFields = new HashMap<>();
367        
368        Collection< ? extends ModelItem> fields = userDirectory.getModelItems();
369        
370        for (ModelItem field : fields)
371        {
372            String fieldId = field.getName();
373            if (!STANDARD_FIELDS.contains(fieldId))
374            {
375                String value = request.getParameter(fieldId);
376                
377                additionalFields.put(fieldId, value);
378            }
379        }
380        
381        return additionalFields;
382    }
383}