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.io.UnsupportedEncodingException;
019import java.net.URLEncoder;
020import java.sql.Timestamp;
021import java.time.LocalDate;
022import java.time.ZoneId;
023import java.time.ZonedDateTime;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.UUID;
030
031import javax.mail.MessagingException;
032
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.commons.lang.StringUtils;
038import org.apache.ibatis.session.SqlSession;
039
040import org.ametys.cms.transformation.URIResolver;
041import org.ametys.cms.transformation.URIResolverExtensionPoint;
042import org.ametys.core.datasource.AbstractMyBatisDAO;
043import org.ametys.core.user.InvalidModificationException;
044import org.ametys.core.user.User;
045import org.ametys.core.user.UserManager;
046import org.ametys.core.user.directory.ModifiableUserDirectory;
047import org.ametys.core.user.directory.UserDirectory;
048import org.ametys.core.user.population.UserPopulation;
049import org.ametys.core.user.population.UserPopulationDAO;
050import org.ametys.core.util.I18nUtils;
051import org.ametys.core.util.mail.SendMailHelper;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.query.expression.Expression;
055import org.ametys.plugins.repository.query.expression.Expression.Operator;
056import org.ametys.runtime.i18n.I18nizableText;
057import org.ametys.runtime.parameter.Errors;
058import org.ametys.web.repository.page.Page;
059import org.ametys.web.repository.page.PageQueryHelper;
060import org.ametys.web.repository.site.Site;
061import org.ametys.web.repository.site.SiteManager;
062import org.ametys.web.site.SiteConfigurationExtensionPoint;
063import org.ametys.web.tags.TagExpression;
064
065/**
066 * Manages registration and password recovery of users.
067 */
068public class UserSignupManager extends AbstractMyBatisDAO
069{
070
071    /** The component role. */
072    public static final String ROLE = UserSignupManager.class.getName();
073
074    /** Return code which indicates no error. */
075    public static final int SIGNUP_NO_ERROR = 0;
076
077    /** Temporary signup return code: a user tried to sign-up but the e-mail already exists in the temporary table. */
078    public static final int SIGNUP_ERROR_TEMP_EMAIL_ALREADY_EXISTS = 1;
079
080    /** Temporary signup return code: a user tried to sign-up but the FO UsersManager already possesses a user with this e-mail as login. */
081    public static final int SIGNUP_ERROR_USER_ALREADY_EXISTS = 2;
082
083    /** Token return code: a user provided a token, but it doesn't exist. */
084    public static final int SIGNUP_TOKEN_UNKNOWN = 3;
085
086    /** Token return code: a user provided a token, but it itn't valid anymore. */
087    public static final int SIGNUP_TOKEN_EXPIRED = 4;
088
089    /** Reset token return code: a user asked for a new token, but no subscription request can be found with this e-mail. */
090    public static final int SIGNUP_RESET_ERROR_EMAIL_UNKNOWN = 5;
091
092    /** Lost password return code: the login or e-mail the user provided doesn't correspond to a population. */
093    public static final int LOST_PASSWORD_USER_UNKNOWN = 5;
094
095    /** Lost password return code: the population the user provided doesn't correspond to a user. */
096    public static final int LOST_PASSWORD_POPULATION_UNKNOWN = 6;
097    
098    /** Lost password return code: the user provided belongs to an unmodifiable user directory */
099    public static final int LOST_PASSWORD_UNMODIFIABLE_USER_DIRECTORY = 7;
100    
101    /** Lost password return code: the user provided have an empty email */
102    public static final int LOST_PASSWORD_EMPTY_EMAIL = 8;
103    
104    /** The user manager. */
105    protected UserManager _userManager;
106    
107    /** The DAO for user populations */
108    protected UserPopulationDAO _userPopulationDAO;
109
110    /** The site manager. */
111    protected SiteManager _siteManager;
112
113    /** The site configuration extension point. */
114    protected SiteConfigurationExtensionPoint _siteConf;
115
116    /** The ametys object resolver. */
117    protected AmetysObjectResolver _resolver;
118
119    /** The ametys object resolver. */
120    protected URIResolverExtensionPoint _uriResolverEP;
121
122    /** The page URI resolver. */
123    protected URIResolver _pageUriResolver;
124
125    /** The i18n utils. */
126    protected I18nUtils _i18nUtils;
127
128    /** The user sign up configuration */
129    protected UserSignUpConfiguration _userSignUpConfiguration;
130    
131    /** The temporary users table. */
132    protected String _tempUsersTable;
133
134    /** The password change table. */
135    protected String _pwdChangeTable;
136    
137    @Override
138    public void service(ServiceManager serviceManager) throws ServiceException
139    {
140        super.service(serviceManager);
141        
142        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
143        _userPopulationDAO = (UserPopulationDAO) serviceManager.lookup(UserPopulationDAO.ROLE);
144        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
145        _siteConf = (SiteConfigurationExtensionPoint) serviceManager.lookup(SiteConfigurationExtensionPoint.ROLE);
146        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
147        _uriResolverEP = (URIResolverExtensionPoint) serviceManager.lookup(URIResolverExtensionPoint.ROLE);
148        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
149        _userSignUpConfiguration = (UserSignUpConfiguration) serviceManager.lookup(UserSignUpConfiguration.ROLE);
150    }
151
152    @Override
153    public void configure(Configuration configuration) throws ConfigurationException
154    {
155        super.configure(configuration);
156        
157        // Configure pool and tables.
158        _tempUsersTable = configuration.getChild("temp-users-table").getValue();
159        _pwdChangeTable = configuration.getChild("pwd-change-table").getValue();
160    }
161
162    /**
163     * Test if public signup is allowed in a site.
164     * @param siteName the site to test.
165     * @return true if public signup is allowed, false otherwise.
166     */
167    public boolean isPublicSignupAllowed(String siteName)
168    {
169        Boolean publicSignup = _siteConf.getValueAsBoolean(siteName, "public-signup");
170
171        return publicSignup != null && publicSignup;
172    }
173
174    /**
175     * Get the sign-up page in a given site and sitemap.<br>
176     * If more than one page are tagged "sign-up", return the first.
177     * @param siteName the site name.
178     * @param language the sitemap name.
179     * @return the sign-up page or null if not found.
180     */
181    public Page getSignupPage(String siteName, String language)
182    {
183        Page page = null;
184
185        try (AmetysObjectIterable<Page> pages = getSignupPages(siteName, language);)
186        {
187            Iterator<Page> it = pages.iterator();
188            if (it.hasNext())
189            {
190                page = it.next();
191            }
192        }
193
194        return page;
195    }
196
197    /**
198     * Get all the pages tagged "sign-up".
199     * @param siteName the site name.
200     * @param language the sitemap name.
201     * @return an iterable on all the pages tagged "sign-up".
202     */
203    public AmetysObjectIterable<Page> getSignupPages(String siteName, String language)
204    {
205        Expression expression = new TagExpression(Operator.EQ, "USER_SIGNUP");
206        String query = PageQueryHelper.getPageXPathQuery(siteName, language, null, expression, null);
207
208        return _resolver.query(query);
209    }
210
211    /**
212     * Get the password change page in a given site and sitemap.
213     * If more than one page are tagged "password change", return the first.
214     * @param siteName the site name.
215     * @param language the sitemap name.
216     * @return the password change page or null if not found.
217     */
218    public Page getPwdChangePage(String siteName, String language)
219    {
220        Page page = null;
221
222        try (AmetysObjectIterable<Page> pages = getPwdChangePages(siteName, language);)
223        {
224            Iterator<Page> it = pages.iterator();
225            if (it.hasNext())
226            {
227                page = it.next();
228            }
229        }
230
231        return page;
232    }
233
234    /**
235     * Get all the pages tagged "password change".
236     * @param siteName the site name.
237     * @param language the sitemap name.
238     * @return an iterable on all the pages tagged "password change".
239     */
240    public AmetysObjectIterable<Page> getPwdChangePages(String siteName, String language)
241    {
242        Expression expression = new TagExpression(Operator.EQ, "USER_PASSWORD_CHANGE");
243        String query = PageQueryHelper.getPageXPathQuery(siteName, language, null, expression, null);
244
245        return _resolver.query(query);
246    }
247
248    /**
249     * Get the user main preferences page in a given site and sitemap.
250     * If more than one page are tagged "user main preferences", return the first.
251     * @param siteName the site name.
252     * @param language the sitemap name.
253     * @return the user main preferences page or null if not found.
254     */
255    public Page getUserMainPrefsPage(String siteName, String language)
256    {
257        Page page = null;
258
259        try (AmetysObjectIterable<Page> pages = getUserMainPrefsPages(siteName, language);)
260        { 
261            Iterator<Page> it = pages.iterator();
262            if (it.hasNext())
263            {
264                page = it.next();
265            }
266        }
267
268        return page;
269    }
270
271    /**
272     * Get all the pages tagged "user main preferences".
273     * @param siteName the site name.
274     * @param language the sitemap name.
275     * @return an iterable on all the pages tagged "user main preferences".
276     */
277    public AmetysObjectIterable<Page> getUserMainPrefsPages(String siteName, String language)
278    {
279        Expression expression = new TagExpression(Operator.EQ, "USER_PREFS_MAIN");
280        String query = PageQueryHelper.getPageXPathQuery(siteName, language, null, expression, null);
281
282        return _resolver.query(query);
283    }
284
285    /**
286     * Tests if the FO users manager knows of the user with the given e-mail as the login.
287     * @param email the e-mail to test.
288     * @param population The id of the population
289     * @return true if the user exists, false otherwise.
290     * @throws UserManagementException if an error occurs.
291     */
292    public boolean userExists(String email, String population) throws UserManagementException
293    {
294        return _userManager.getUser(population, email) != null;
295    }
296
297    /**
298     * Validate the user subscription data.
299     * @param siteName the site name.
300     * @param email the user e-mail.
301     * @param firstName the user first name.
302     * @param lastName the user last name.
303     * @param additionalValues the additional user values.
304     * @return a Map of the Errors by field.
305     * @throws UserManagementException if an error occurs.
306     */
307    public Map<String, Errors> validate(String siteName, String email, String firstName, String lastName, Map<String, String> additionalValues) throws UserManagementException
308    {
309        Map<String, String> userInfos = new HashMap<>();
310        
311        userInfos.putAll(additionalValues);
312        
313        // Standard info for user (provide a dummy password, as we do not know it yet).
314        userInfos.put("login", email);
315        userInfos.put("email", email);
316        userInfos.put("firstname", firstName);
317        userInfos.put("lastname", lastName);
318        userInfos.put("password", "password");
319
320        Map<String, Errors> usersManagerErrors = new HashMap<>(); //foUsersManager.validate(userInfos);
321        Map<String, Errors> errors = new HashMap<>(usersManagerErrors);
322
323        // If there are errors on the login, do not return it except if
324        if (errors.containsKey("login"))
325        {
326            if (!errors.containsKey("email"))
327            {
328                errors.put("email", errors.get("login"));
329            }
330            errors.remove("login");
331        }
332
333        return errors;
334    }
335
336    /**
337     * Validate the user password.
338     * @param siteName the site name.
339     * @param password the password to validate.
340     * @param login the login of the user
341     * @param population The id of the population
342     * @return a Map of the Errors by field.
343     * @throws UserManagementException if an error occurs.
344     */
345    public Map<String, Errors> validatePassword(String siteName, String password, String login, String population) throws UserManagementException
346    {
347        Map<String, String> userInfos = new HashMap<>();
348
349        userInfos.put("password", password);
350
351        UserDirectory userDirectory = _userManager.getUserDirectory(population, login);
352        if (!(userDirectory instanceof ModifiableUserDirectory))
353        {
354            throw new UserManagementException("The user subscription feature can't be used, as the UserDirectory is not modifiable.");
355        }
356        Map<String, Errors> usersManagerErrors = ((ModifiableUserDirectory) userDirectory).validate(userInfos);
357        Map<String, Errors> errors = new HashMap<>();
358
359        // Keep only errors related to the password field.
360        if (usersManagerErrors.containsKey("password"))
361        {
362            errors.put("password", usersManagerErrors.get("password"));
363        }
364
365        return errors;
366    }
367
368    /**
369     * Validate and store a sign-up request and send a confirmation e-mail.
370     * @param siteName the site name.
371     * @param language the sitemap name.
372     * @param email the user e-mail address.
373     * @param firstName the user first name.
374     * @param lastName the user last name.
375     * @param population the population
376     * @param userDirectoryId the id of the user directory of the population
377     * @return a status code indicating success or error.
378     * @throws UserManagementException if an error occurs.
379     */
380    public int temporarySignup(String siteName, String language, String email, String firstName, String lastName, String population, String userDirectoryId) throws UserManagementException
381    {
382        // Verify that public sign-up is allowed and throw an exception otherwise.
383        checkPublicSignup(siteName);
384
385        if (getLogger().isDebugEnabled())
386        {
387            getLogger().debug("A user is requesting a sign-up: " + firstName + " " + lastName + " (" + email + ").");
388        }
389
390        // Generate unique token.
391        String token = UUID.randomUUID().toString().replace("-", "");
392
393        int status = addTemporaryUser(siteName, email, firstName, lastName, token, population, userDirectoryId);
394
395        // Send validation e-mail with token.
396        if (status == SIGNUP_NO_ERROR)
397        {
398            sendSignupConfirmMail(siteName, language, email, firstName, lastName, token);
399        }
400
401        return status;
402    }
403
404    /**
405     * Reset a sign-up request: generate a new token and re-send the confirmation e-mail.
406     * @param siteName the site name.
407     * @param language the sitemap name.
408     * @param email the user e-mail address.
409     * @param population The id of the population
410     * @param userDirectoryId The id of the user directory of the population
411     * @return a status code indicating success or error.
412     * @throws UserManagementException if an error occurs.
413     */
414    public int resetTempSignup(String siteName, String language, String email, String population, String userDirectoryId) throws UserManagementException
415    {
416        // Verify that public sign-up is allowed and throw an exception otherwise.
417        checkPublicSignup(siteName);
418
419        if (getLogger().isDebugEnabled())
420        {
421            getLogger().debug("Resetting temporary signup for email: " + email + " in site " + siteName);
422        }
423
424        // Generate a new token.
425        String newToken = UUID.randomUUID().toString().replace("-", "");
426
427        // Test if the subscription request really exists.
428        TempUser tempUser = getTempUser(siteName, email, population, userDirectoryId);
429
430        if (tempUser == null)
431        {
432            // No subscription request with this e-mail.
433            return SIGNUP_RESET_ERROR_EMAIL_UNKNOWN;
434        }
435
436        updateTempToken(siteName, email, newToken, population, userDirectoryId);
437
438        sendSignupConfirmMail(siteName, language, email, tempUser.getFirstname(), tempUser.getLastname(), newToken);
439
440        return SIGNUP_NO_ERROR;
441    }
442
443
444    /**
445     * Create the user in the FO UsersManager from his temporary request.
446     * @param siteName the site name.
447     * @param email the user e-mail address.
448     * @param token the request token.
449     * @param password the user password.
450     * @param population The id of the population
451     * @param userDirectoryId The id of the user directory of the population
452     * @return a status code indicating success or error.
453     * @throws UserManagementException if an error occurs.
454     */
455    public int signup(String siteName, String email, String token, String password, String population, String userDirectoryId) throws UserManagementException
456    {
457        // Verify that public sign-up is allowed and throw an exception otherwise.
458        checkPublicSignup(siteName);
459
460        Map<String, String> userInfos = new HashMap<>();
461
462        TempUser tempUser = getTempUser(siteName, email, token, population, userDirectoryId);
463
464        if (tempUser == null)
465        {
466            return SIGNUP_TOKEN_UNKNOWN;
467        }
468
469        // Standard info for user.
470        userInfos.put("login", email);
471        userInfos.put("email", email);
472        userInfos.put("firstname", tempUser.getFirstname());
473        userInfos.put("lastname", tempUser.getLastname());
474        userInfos.put("password", password);
475
476        try
477        {
478            // Add the user.
479            ModifiableUserDirectory userDirectory = (ModifiableUserDirectory) _userPopulationDAO.getUserPopulation(population).getUserDirectory(userDirectoryId);
480            userDirectory.add(userInfos);
481
482            // Remove the temporary user.
483            removeTempUser(siteName, email, token, population, userDirectoryId);
484
485            return SIGNUP_NO_ERROR;
486        }
487        catch (InvalidModificationException e)
488        {
489            throw new UserManagementException("An error occurred signing up the user.", e);
490        }
491    }
492
493    /**
494     * Create a reset password request and send a confirmation e-mail.
495     * @param siteName the site name.
496     * @param language the sitemap name.
497     * @param login the user login.
498     * @param populationId the population
499     * @return a status code indicating success or error.
500     * @throws UserManagementException if an error occurs.
501     */
502    public int resetPassword(String siteName, String language, String login, String populationId) throws UserManagementException
503    {
504        // Check if the population exists
505        UserPopulation population = _userPopulationDAO.getUserPopulation(populationId);
506        if (population == null)
507        {
508            return LOST_PASSWORD_POPULATION_UNKNOWN;
509        }
510
511        // Check if the user exists and get it
512        User user = _userManager.getUser(populationId, login);
513        if (user == null)
514        {
515            // No user with this e-mail as login.
516            return LOST_PASSWORD_USER_UNKNOWN;
517        }
518
519        // Check if the user directory is modifiable
520        if (!(user.getUserDirectory() instanceof ModifiableUserDirectory))
521        {
522            return LOST_PASSWORD_UNMODIFIABLE_USER_DIRECTORY;
523        }
524        
525        if (StringUtils.isEmpty(user.getEmail()))
526        {
527            return LOST_PASSWORD_EMPTY_EMAIL;
528        }
529        
530        // Generate a new token.
531        String token = UUID.randomUUID().toString().replace("-", "");
532
533        // Insert the token in the database.
534        addPasswordToken(siteName, login, token, populationId);
535
536        // Send the e-mail.
537        sendResetPasswordMail(siteName, language, user, token);
538
539        return SIGNUP_NO_ERROR;
540    }
541
542    /**
543     * Change the user password.
544     * @param siteName the site name.
545     * @param login the user login.
546     * @param token the password change request token.
547     * @param newPassword the new password.
548     * @param population the population
549     * @return a status code indicating success or error.
550     * @throws UserManagementException if an error occurs.
551     */
552    public int changeUserPassword(String siteName, String login, String token, String newPassword, String population) throws UserManagementException
553    {
554        int tokenStatus = checkPasswordToken(siteName, login, token, population);
555
556        if (tokenStatus != SIGNUP_NO_ERROR)
557        {
558            return tokenStatus;
559        }
560
561        Map<String, String> userInfos = new HashMap<>();
562
563        userInfos.put("login", login);
564        userInfos.put("password", newPassword);
565
566        try
567        {
568            UserDirectory userDirectory = _userManager.getUserDirectory(population, login);
569            if (!(userDirectory instanceof ModifiableUserDirectory))
570            {
571                throw new UserManagementException("The user's password can't be changed, as the UserDirectory is not modifiable.");
572            }
573            ((ModifiableUserDirectory) userDirectory).update(userInfos);
574
575            removePasswordToken(siteName, login, token, population);
576
577            return SIGNUP_NO_ERROR;
578        }
579        catch (InvalidModificationException e)
580        {
581            throw new UserManagementException("An error occurred signing up the user.", e);
582        }
583    }
584
585    /**
586     * Check the sign-up request token.
587     * @param siteName the site name.
588     * @param email the user e-mail.
589     * @param token the sign-up request token.
590     * @param population The id of the population
591     * @param userDirectoryId The id of the user directory of the population
592     * @return a status code indicating success or error.
593     * @throws UserManagementException if an error occurs.
594     */
595    public int checkToken(String siteName, String email, String token, String population, String userDirectoryId) throws UserManagementException
596    {
597        removeExpiredTokens();
598
599        try (SqlSession sqlSession = getSession())
600        {
601            String stmtId = "UserSignupManager.getSubscriptionDate";
602            
603            Map<String, Object> params = new HashMap<>();
604            params.put("tempUsersTable", _tempUsersTable);
605            
606            params.put("site", siteName);
607            params.put("email", email);
608            params.put("token", token);
609            params.put("population", population);
610            params.put("userDirectory", userDirectoryId);
611            
612            Date rawSubscriptionDate = sqlSession.selectOne(stmtId, params);
613
614            // Date verification.
615            if (rawSubscriptionDate == null)
616            {
617                return SIGNUP_TOKEN_UNKNOWN;
618            }
619            
620            // The validity limit
621            ZonedDateTime limit = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).minusDays(_userSignUpConfiguration.getTokenValidity());
622            ZonedDateTime subscriptionDate = rawSubscriptionDate.toInstant().atZone(ZoneId.systemDefault());
623
624            if (subscriptionDate.isBefore(limit))
625            {
626                return SIGNUP_TOKEN_EXPIRED;
627            }
628
629            return SIGNUP_NO_ERROR;
630        }
631        catch (Exception e)
632        {
633            throw new UserManagementException("Database error while testing the token for user [" + email + "]", e);
634        }
635    }
636
637    /**
638     * Check the password change request token.
639     * @param siteName the site name.
640     * @param login the user login.
641     * @param token the password change request token.
642     * @param population the population
643     * @return a status code indicating success or error.
644     * @throws UserManagementException if an error occurs.
645     */
646    public int checkPasswordToken(String siteName, String login, String token, String population) throws UserManagementException
647    {
648        removeExpiredPasswordTokens();
649
650        try (SqlSession sqlSession = getSession())
651        {
652            String stmtId = "UserSignupManager.getRequestDate";
653            
654            Map<String, Object> params = new HashMap<>();
655            params.put("pwdChangeTable", _pwdChangeTable);
656            
657            params.put("site", siteName);
658            params.put("login", login);
659            params.put("token", token);
660            params.put("population", population);
661            
662            Date rawRequestDate = sqlSession.selectOne(stmtId, params);
663
664            // Date verification.
665            if (rawRequestDate == null)
666            {
667                return SIGNUP_TOKEN_UNKNOWN;
668            }
669
670            // Check the validity.
671            ZonedDateTime limit = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).minusDays(_userSignUpConfiguration.getTokenValidity());
672            ZonedDateTime requestDate = rawRequestDate.toInstant().atZone(ZoneId.systemDefault());
673
674            if (requestDate.isBefore(limit))
675            {
676                return SIGNUP_TOKEN_EXPIRED;
677            }
678
679            return SIGNUP_NO_ERROR;
680        }
681        catch (Exception e)
682        {
683            throw new UserManagementException("Database error while testing the password token for user " + login, e);
684        }
685    }
686
687    /**
688     * Remove the expired sign-up request tokens.
689     * @throws UserManagementException if an error occurs.
690     */
691    public void removeExpiredTokens() throws UserManagementException
692    {
693        try (SqlSession sqlSession = getSession())
694        {
695            String stmtId = "UserSignupManager.deleteExpiredTokens";
696            
697            Map<String, Object> params = new HashMap<>();
698            params.put("tempUsersTable", _tempUsersTable);
699            
700            ZonedDateTime limit = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).minusDays(_userSignUpConfiguration.getTokenValidity());
701            Timestamp limitTimestamp = Timestamp.from(limit.toInstant());
702            params.put("threshold", limitTimestamp);
703            
704            sqlSession.delete(stmtId, params);
705            sqlSession.commit();
706        }
707        catch (Exception e)
708        {
709            throw new UserManagementException("Database error while removing the expired tokens.", e);
710        }
711    }
712
713    /**
714     * Remove the expired change password request tokens.
715     * @throws UserManagementException if an error occurs.
716     */
717    public void removeExpiredPasswordTokens() throws UserManagementException
718    {
719        try (SqlSession sqlSession = getSession())
720        {
721            String stmtId = "UserSignupManager.deleteExpiredPasswordTokens";
722            
723            Map<String, Object> params = new HashMap<>();
724            params.put("pwdChangeTable", _pwdChangeTable);
725            
726            ZonedDateTime limit = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).minusDays(_userSignUpConfiguration.getTokenValidity());
727            Timestamp limitTimestamp = Timestamp.from(limit.toInstant());
728            params.put("threshold", limitTimestamp);
729            
730            sqlSession.delete(stmtId, params);
731            sqlSession.commit();
732        }
733        catch (Exception e)
734        {
735            throw new UserManagementException("Database error while removing the expired tokens.", e);
736        }
737    }
738
739    /**
740     * Verify that public sign-up is allowed. If not, throw an exception.
741     * @param siteName the site name.
742     * @throws UserManagementException if public sign-up is not enabled.
743     */
744    protected void checkPublicSignup(String siteName) throws UserManagementException
745    {
746        if (!isPublicSignupAllowed(siteName))
747        {
748            throw new UserManagementException("Public signup is disabled for this site.");
749        }
750    }
751
752    /**
753     * Create a user sign-up request ("temporary" user) in the database.
754     * @param siteName the site name.
755     * @param email the user e-mail.
756     * @param firstName the user first name.
757     * @param lastName the user last name.
758     * @param token the generated token.
759     * @param population the population
760     * @param userDirectoryId the id of the user directory of the population
761     * @return a status code indicating success or error.
762     * @throws UserManagementException if an error occurs.
763     */
764    protected int addTemporaryUser(String siteName, String email, String firstName, String lastName, String token, String population, String userDirectoryId) throws UserManagementException
765    {
766        try (SqlSession sqlSession = getSession())
767        {
768            // Does this email already exists
769            String stmtId = "UserSignupManager.tempEmailExists";
770            
771            Map<String, Object> params = new HashMap<>();
772            params.put("tempUsersTable", _tempUsersTable);
773            
774            params.put("site", siteName);
775            params.put("email", email);
776            params.put("population", population);
777            params.put("userDirectory", userDirectoryId);
778            
779            List<String> emails = sqlSession.selectList(stmtId, params);
780            
781            if (!emails.isEmpty())
782            {
783                return SIGNUP_ERROR_TEMP_EMAIL_ALREADY_EXISTS;
784            }
785            
786            // Add temp user
787            stmtId = "UserSignupManager.addTempUser";
788            params = new HashMap<>();
789            params.put("tempUsersTable", _tempUsersTable);
790            
791            Timestamp now = new Timestamp(System.currentTimeMillis());
792            
793            params.put("site", siteName);
794            params.put("email", email);
795            params.put("population", population);
796            params.put("userDirectory", userDirectoryId);
797            params.put("firstname", firstName);
798            params.put("lastname", lastName);
799            params.put("subscription_date", now);
800            params.put("token", token);
801            
802            sqlSession.insert(stmtId, params);
803            sqlSession.commit();
804            
805            return SIGNUP_NO_ERROR;
806        }
807        catch (Exception e)
808        {
809            throw new UserManagementException("Database error while signing up a new user [" + email + "]", e);
810        }
811    }
812
813    /**
814     * Send a sign-up confirmation link by e-mail.
815     * @param siteName the site name.
816     * @param language the e-mail language.
817     * @param email the user e-mail.
818     * @param firstName the user first name.
819     * @param lastName the user last name.
820     * @param token the generated token.
821     * @throws UserManagementException if an error occurs.
822     */
823    protected void sendSignupConfirmMail(String siteName, String language, String email, String firstName, String lastName, String token) throws UserManagementException
824    {
825        if (getLogger().isDebugEnabled())
826        {
827            getLogger().debug("Sending signup confirmation e-mail to " + email);
828        }
829
830        String from = _siteConf.getValueAsString(siteName, "site-mail-from");
831
832        // Prepare mail.
833        Map<String, I18nizableText> i18nParams = new HashMap<>();
834        i18nParams.put("siteName", new I18nizableText(siteName));
835        i18nParams.put("email", new I18nizableText(email));
836        i18nParams.put("firstName", new I18nizableText(firstName));
837        i18nParams.put("lastName", new I18nizableText(lastName));
838        i18nParams.put("token", new I18nizableText(token));
839
840        Page signupPage = getSignupPage(siteName, language);
841        if (signupPage != null)
842        {
843            if (_pageUriResolver == null)
844            {
845                _pageUriResolver = _uriResolverEP.getResolverForType("page");
846            }
847
848            String encodedEmail;
849            try
850            {
851                encodedEmail = URLEncoder.encode(email, "UTF-8");
852            }
853            catch (UnsupportedEncodingException e)
854            {
855                // Should never happen.
856                throw new UserManagementException("Encoding error while sending a sign-up confirmation e-mail.", e);
857            }
858
859            // Compute the confirmation URI and add it to the parameters.
860            String confirmUri = _pageUriResolver.resolve(signupPage.getId(), false, true, false);
861            confirmUri = confirmUri + "?email=" + encodedEmail + "&token=" + token;
862
863            i18nParams.put("confirmUri", new I18nizableText(confirmUri));
864        }
865
866        // Add site information in the parameters.
867        Site site = _siteManager.getSite(siteName);
868        if (site != null)
869        {
870            i18nParams.put("siteTitle", new I18nizableText(site.getTitle()));
871            i18nParams.put("siteUrl", new I18nizableText(site.getUrl()));
872        }
873
874        String subject = _userSignUpConfiguration.getSubjectForSignUpEmail(i18nParams, language);
875        String textBody = _userSignUpConfiguration.getTextBodyForSignUpEmail(i18nParams, language);
876        String htmlBody = _userSignUpConfiguration.getHtmlBodyForSignUpEmail(i18nParams, language);
877
878        try
879        {
880            // Send the e-mail.
881            SendMailHelper.sendMail(subject, htmlBody, textBody, email, from);
882        }
883        catch (MessagingException e)
884        {
885            throw new UserManagementException("Error sending the sign-up confirmation mail.", e);
886        }
887    }
888
889    /**
890     * Update the sign-up request token: reset the date and set a new token.
891     * @param siteName the site name.
892     * @param email the user e-mail.
893     * @param newToken the new token.
894     * @param population The id of the population
895     * @param userDirectoryId The id of the user directory of the population
896     * @throws UserManagementException if an error occurs.
897     */
898    protected void updateTempToken(String siteName, String email, String newToken, String population, String userDirectoryId) throws UserManagementException
899    {
900        try (SqlSession sqlSession = getSession())
901        {
902            String stmtId = "UserSignupManager.updateTempToken";
903            
904            Map<String, Object> params = new HashMap<>();
905            params.put("tempUsersTable", _tempUsersTable);
906            
907            Timestamp now = new Timestamp(System.currentTimeMillis());
908            params.put("subscription_date", now);
909            params.put("token", newToken);
910            params.put("site", siteName);
911            params.put("email", email);
912            params.put("population", population);
913            params.put("userDirectory", userDirectoryId);
914            
915            sqlSession.update(stmtId, params);
916            sqlSession.commit();
917        }
918        catch (Exception e)
919        {
920            throw new UserManagementException("Database error while resetting the subscription for user [" + email + "]", e);
921        }
922    }
923
924    /**
925     * Create a user password change request in the database.
926     * @param siteName the site name.
927     * @param login the user login.
928     * @param token the generated token.
929     * @param population the population
930     * @throws UserManagementException if an error occurs.
931     */
932    protected void addPasswordToken(String siteName, String login, String token, String population) throws UserManagementException
933    {
934        try (SqlSession sqlSession = getSession())
935        {
936            String stmtId = "UserSignupManager.hasToken";
937            
938            Map<String, Object> params = new HashMap<>();
939            params.put("pwdChangeTable", _pwdChangeTable);
940            
941            params.put("site", siteName);
942            params.put("login", login);
943            params.put("population", population);
944            
945            List<Object> pwdEntries = sqlSession.selectList(stmtId, params);
946            
947            if (pwdEntries.isEmpty())
948            {
949                // Insert a new token.
950                stmtId = "UserSignupManager.addPasswordToken";
951                params = new HashMap<>();
952                params.put("pwdChangeTable", _pwdChangeTable);
953                
954                Timestamp now = new Timestamp(System.currentTimeMillis());
955                params.put("site", siteName);
956                params.put("login", login);
957                params.put("request_date", now);
958                params.put("token", token);
959                params.put("population", population);
960                
961                sqlSession.insert(stmtId, params);
962            }
963            else
964            {
965                // Update the existing token.
966                stmtId = "UserSignupManager.updatePasswordToken";
967                params = new HashMap<>();
968                params.put("pwdChangeTable", _pwdChangeTable);
969                
970                Timestamp now = new Timestamp(System.currentTimeMillis());
971                params.put("request_date", now);
972                params.put("token", token);
973                params.put("site", siteName);
974                params.put("login", login);
975                params.put("population", population);
976                
977                sqlSession.update(stmtId, params);
978            }
979            
980            // commit insert or update
981            sqlSession.commit();
982        }
983        catch (Exception e)
984        {
985            throw new UserManagementException("Database error while inserting a password change token for " + login, e);
986        }
987    }
988
989    /**
990     * Get a temporary user from his site name and e-mail.
991     * @param siteName the site name.
992     * @param email The temporary user e-mail. Cannot be null.
993     * @param population the population
994     * @param userDirectoryId the id of the user directory of the population
995     * @return the temporary user or null if not found.
996     * @throws UserManagementException if an error occurs.
997     */
998    protected TempUser getTempUser(String siteName, String email, String population, String userDirectoryId) throws UserManagementException
999    {
1000        return getTempUser(siteName, email, null, population, userDirectoryId);
1001    }
1002
1003    /**
1004     * Get a temporary user from his site name, e-mail and/or token.
1005     * At least one of e-mail and token must be provided.
1006     * @param siteName the site name.
1007     * @param email The temporary user e-mail. Can be null.
1008     * @param token The temporary user token. Can be null.
1009     * @param population the population. Must be not null if email is not null
1010     * @param userDirectoryId the id of the user directory of the population. Must be not null if email is not null
1011     * @return the temporary user or null if not found.
1012     * @throws UserManagementException if an error occurs.
1013     */
1014    protected TempUser getTempUser(String siteName, String email, String token, String population, String userDirectoryId) throws UserManagementException
1015    {
1016        if (StringUtils.isEmpty(email) && StringUtils.isEmpty(token))
1017        {
1018            throw new UserManagementException("Either e-mail or token must be provided.");
1019        }
1020        
1021        try (SqlSession sqlSession = getSession())
1022        {
1023            // Does this email already exists
1024            String stmtId = "UserSignupManager.getTempUser";
1025            
1026            Map<String, Object> params = new HashMap<>();
1027            params.put("tempUsersTable", _tempUsersTable);
1028            
1029            params.put("site", siteName);
1030            if (StringUtils.isNotEmpty(email))
1031            {
1032                params.put("email", email);
1033                params.put("population", population);
1034                params.put("userDirectory", userDirectoryId);
1035            }
1036            if (StringUtils.isNotEmpty(token))
1037            {
1038                params.put("token", token);
1039            }
1040            
1041            return sqlSession.selectOne(stmtId, params);
1042        }
1043        catch (Exception e)
1044        {
1045            throw new UserManagementException("Database error while getting the request for user [" + email + "] and token [" + token + "]", e);
1046        }
1047    }
1048
1049    /**
1050     * Remove the temporary .
1051     * @param siteName the site name.
1052     * @param email the user e-mail address.
1053     * @param token the request token.
1054     * @param population the population
1055     * @param userDirectoryId the id of the user directory of the population
1056     * @throws UserManagementException if an error occurs.
1057     */
1058    protected void removeTempUser(String siteName, String email, String token, String population, String userDirectoryId) throws UserManagementException
1059    {
1060        try (SqlSession sqlSession = getSession())
1061        {
1062            String stmtId = "UserSignupManager.removeTempUser";
1063            
1064            Map<String, Object> params = new HashMap<>();
1065            params.put("tempUsersTable", _tempUsersTable);
1066            
1067            params.put("site", siteName);
1068            params.put("email", email);
1069            params.put("token", token);
1070            params.put("population", population);
1071            params.put("userDirectory", userDirectoryId);
1072            
1073            sqlSession.delete(stmtId, params);
1074            sqlSession.commit();
1075        }
1076        catch (Exception e)
1077        {
1078            throw new UserManagementException("Database error while removing the token of user " + email, e);
1079        }
1080    }
1081
1082    /**
1083     * Remove the password change request.
1084     * @param siteName the site name.
1085     * @param login the user login.
1086     * @param token the request token.
1087     * @param population the population
1088     * @throws UserManagementException if an error occurs.
1089     */
1090    protected void removePasswordToken(String siteName, String login, String token, String population) throws UserManagementException
1091    {
1092        try (SqlSession sqlSession = getSession())
1093        {
1094            String stmtId = "UserSignupManager.removePasswordToken";
1095            
1096            Map<String, Object> params = new HashMap<>();
1097            params.put("pwdChangeTable", _pwdChangeTable);
1098            
1099            params.put("site", siteName);
1100            params.put("login", login);
1101            params.put("token", token);
1102            params.put("population", population);
1103            
1104            sqlSession.delete(stmtId, params);
1105            sqlSession.commit();
1106        }
1107        catch (Exception e)
1108        {
1109            throw new UserManagementException("Database error while removing the token of user " + login, e);
1110        }
1111    }
1112
1113    /**
1114     * Send a sign-up confirmation link by e-mail.
1115     * @param siteName the site name.
1116     * @param language the e-mail language.
1117     * @param user the user object.
1118     * @param token the generated token.
1119     * @throws UserManagementException if an error occurs.
1120     */
1121    protected void sendResetPasswordMail(String siteName, String language, User user, String token) throws UserManagementException
1122    {
1123        String login = user.getIdentity().getLogin();
1124        String population = user.getIdentity().getPopulationId();
1125
1126        if (getLogger().isDebugEnabled())
1127        {
1128            getLogger().debug("Sending reset password e-mail to " + login);
1129        }
1130
1131        String from = _siteConf.getValueAsString(siteName, "site-mail-from");
1132
1133        // Prepare mail
1134        Map<String, I18nizableText> i18nParams = new HashMap<>();
1135        i18nParams.put("siteName", new I18nizableText(siteName));
1136        i18nParams.put("login", new I18nizableText(login));
1137        i18nParams.put("email", new I18nizableText(user.getEmail()));
1138        i18nParams.put("fullName", new I18nizableText(user.getFullName()));
1139        i18nParams.put("token", new I18nizableText(token));
1140
1141        Page passwordPage = getPwdChangePage(siteName, language);
1142        if (passwordPage != null)
1143        {
1144            if (_pageUriResolver == null)
1145            {
1146                _pageUriResolver = _uriResolverEP.getResolverForType("page");
1147            }
1148
1149            String encodedLogin;
1150            String encodedPopulation;
1151            try
1152            {
1153                encodedLogin = URLEncoder.encode(login, "UTF-8");
1154                encodedPopulation = URLEncoder.encode(population, "UTF-8");
1155            }
1156            catch (UnsupportedEncodingException e)
1157            {
1158                // Should never happen.
1159                throw new UserManagementException("Encoding error while sending a password reset confirmation e-mail.", e);
1160            }
1161
1162            // Compute the confirmation URI and add it to the parameters.
1163            String confirmUri = _pageUriResolver.resolve(passwordPage.getId(), false, true, false);
1164            confirmUri = confirmUri + "?login=" + encodedLogin + "&population=" + encodedPopulation + "&token=" + token;
1165
1166            i18nParams.put("confirmUri", new I18nizableText(confirmUri));
1167        }
1168
1169        // Add site information in the parameters.
1170        Site site = _siteManager.getSite(siteName);
1171        if (site != null)
1172        {
1173            i18nParams.put("siteTitle", new I18nizableText(site.getTitle()));
1174            i18nParams.put("siteUrl", new I18nizableText(site.getUrl()));
1175        }
1176
1177        String subject = _userSignUpConfiguration.getSubjectForResetPwdEmail(i18nParams, language);
1178        String textBody = _userSignUpConfiguration.getTextBodyForResetPwdEmail(i18nParams, language);
1179        String htmlBody = _userSignUpConfiguration.getHtmlBodyForResetPwdEmail(i18nParams, language);
1180
1181        try
1182        {
1183            // Send the e-mail.
1184            SendMailHelper.sendMail(subject, htmlBody, textBody, user.getEmail(), from);
1185        }
1186        catch (MessagingException e)
1187        {
1188            throw new UserManagementException("Error sending a password reset e-mail.", e);
1189        }
1190    }
1191
1192    /**
1193     * Bean representing a user sign-up request.
1194     */
1195    public static class TempUser
1196    {
1197        /** The site name. */
1198        protected String _site;
1199        
1200        /** The user e-mail. */
1201        protected String _email;
1202
1203        /** The user first name. */
1204        protected String _firstname;
1205
1206        /** The user last name. */
1207        protected String _lastname;
1208
1209        /** The user subscription date. */
1210        protected Date _subscriptionDate;
1211
1212        /** The request token. */
1213        protected String _token;
1214
1215        /** The id of the population */
1216        protected String _population;
1217
1218        /** The id of the user directory of the population */
1219        protected String _userDirectoryId;
1220
1221        /**
1222         * Constructor.
1223         * @param site the site
1224         * @param email the user's email
1225         * @param firstname the user's firstname
1226         * @param lastname the user's lastname
1227         * @param subscriptionDate the date of subscription
1228         * @param token the user's token
1229         * @param population The id of the population
1230         * @param userDirectoryId The id of the user directory of the population
1231         */
1232        public TempUser(String site, String email, String firstname, String lastname, Date subscriptionDate, String token, String population, String userDirectoryId)
1233        {
1234            this._site = site;
1235            this._email = email;
1236            this._firstname = firstname;
1237            this._lastname = lastname;
1238            this._subscriptionDate = subscriptionDate;
1239            this._token = token;
1240            this._population = population;
1241            this._userDirectoryId = userDirectoryId;
1242        }
1243        /**
1244         * Get the site.
1245         * @return the site
1246         */
1247        public String getSite()
1248        {
1249            return _site;
1250        }
1251        /**
1252         * Set the site.
1253         * @param site the site to set
1254         */
1255        public void setSite(String site)
1256        {
1257            this._site = site;
1258        }
1259        /**
1260         * Get the email.
1261         * @return _the email
1262         */
1263        public String getEmail()
1264        {
1265            return _email;
1266        }
1267        /**
1268         * Set the email.
1269         * @param email the email to set
1270         */
1271        public void setEmail(String email)
1272        {
1273            this._email = email;
1274        }
1275        /**
1276         * Get the firstname.
1277         * @return _the firstname
1278         */
1279        public String getFirstname()
1280        {
1281            return _firstname;
1282        }
1283        /**
1284         * Set the firstname.
1285         * @param firstname the firstname to set
1286         */
1287        public void setFirstname(String firstname)
1288        {
1289            this._firstname = firstname;
1290        }
1291        /**
1292         * Get the lastname.
1293         * @return _the lastname
1294         */
1295        public String getLastname()
1296        {
1297            return _lastname;
1298        }
1299        /**
1300         * Set the lastname.
1301         * @param lastname the lastname to set
1302         */
1303        public void setLastname(String lastname)
1304        {
1305            this._lastname = lastname;
1306        }
1307        /**
1308         * Get the subscriptionDate.
1309         * @return _the subscriptionDate
1310         */
1311        public Date getSubscriptionDate()
1312        {
1313            return _subscriptionDate;
1314        }
1315        /**
1316         * Set the subscriptionDate.
1317         * @param subscriptionDate the subscriptionDate to set
1318         */
1319        public void setSubscriptionDate(Date subscriptionDate)
1320        {
1321            this._subscriptionDate = subscriptionDate;
1322        }
1323        /**
1324         * Get the token.
1325         * @return _the token
1326         */
1327        public String getToken()
1328        {
1329            return _token;
1330        }
1331        /**
1332         * Set the token.
1333         * @param token the token to set
1334         */
1335        public void setToken(String token)
1336        {
1337            this._token = token;
1338        }
1339        /**
1340         * Get the population.
1341         * @return the population
1342         */
1343        public String getPopulation()
1344        {
1345            return _population;
1346        }
1347        /**
1348         * Set the population.
1349         * @param population the population to set
1350         */
1351        public void setPopulation(String population)
1352        {
1353            this._population = population;
1354        }
1355        /**
1356         * Get the user directory id.
1357         * @return the user directory id
1358         */
1359        public String getUserDirectoryId()
1360        {
1361            return _userDirectoryId;
1362        }
1363        /**
1364         * Set the user directory index.
1365         * @param userDirectoryId the user directory id to set
1366         */
1367        public void setUserDirectoryIndex(String userDirectoryId)
1368        {
1369            this._userDirectoryId = userDirectoryId;
1370        }
1371
1372    }
1373
1374}