/* * Copyright 1998-2017 Linux.org.ru * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ru.org.linux.user; import org.jasypt.util.password.BasicPasswordEncryptor; import org.jasypt.util.password.PasswordEncryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import ru.org.linux.util.StringUtil; import ru.org.linux.util.URLUtil; import scala.Tuple2; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.mail.internet.InternetAddress; import javax.sql.DataSource; import java.sql.SQLException; import java.sql.Timestamp; import java.util.List; @Repository public class UserDao { private static final Logger logger = LoggerFactory.getLogger(UserDao.class); private JdbcTemplate jdbcTemplate; @Autowired private UserLogDao userLogDao; /** * изменение score пользователю */ private static final String queryChangeScore = "UPDATE users SET score=score+? WHERE id=?"; private static final String queryUserById = "SELECT id,nick,score,max_score,candel,canmod,corrector,passwd,blocked,activated,photo,email,name,unread_events,style FROM users where id=?"; private static final String queryUserIdByNick = "SELECT id FROM users where nick=?"; private static final String updateUserStyle = "UPDATE users SET style=? WHERE id=?"; private static final String queryNewUsers = "SELECT id FROM users where " + "regdate IS NOT null " + "AND regdate > CURRENT_TIMESTAMP - interval '3 days' " + "ORDER BY regdate"; private static final String queryBanInfoClass = "SELECT * FROM ban_info WHERE userid=?"; private static final String queryCommentStat = "SELECT count(*) as c FROM comments WHERE userid=? AND not deleted"; private static final String queryCommentDates = "SELECT min(postdate) as first,max(postdate) as last FROM comments WHERE comments.userid=?"; @Autowired public void setJdbcTemplate(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } public int findUserId(String nick) throws UserNotFoundException { if (nick == null) { throw new NullPointerException(); } if (!StringUtil.checkLoginName(nick)) { logger.warn("Invalid user name '{}'", nick); throw new UserNotFoundException("<invalid name>"); } List<Integer> list = jdbcTemplate.queryForList( queryUserIdByNick, Integer.class, nick ); if (list.isEmpty()) { throw new UserNotFoundException(nick); } if (list.size()>1) { throw new RuntimeException("list.size()>1 ???"); } return list.get(0); } @Cacheable("Users") public User getUserCached(int id) throws UserNotFoundException { return getUserInternal(id); } /** * Загружает пользователя из БД не используя кеш (всегда обновляет кеш). * Метод используется там, где нужно проверить права пользователя, совершить какой-то * update или получить самый свежий варинт из БД. В остальных случаях нужно * использовать метод getUserCached() * * @param id идентификатор пользователя * @return объект пользователя * @throws UserNotFoundException если пользователь с таким id не найден */ @CachePut("Users") public User getUser(int id) throws UserNotFoundException { return getUserInternal(id); } private User getUserInternal(int id) throws UserNotFoundException { List<User> list = jdbcTemplate.query(queryUserById, (rs, rowNum) -> new User(rs), id); if (list.isEmpty()) { throw new UserNotFoundException(id); } if (list.size() > 1) { throw new RuntimeException("list.size()>1 ???"); } return list.get(0); } /** * Получить поле userinfo пользователя * TODO надо переименовать? * @param user пользователь * @return поле userinfo */ public String getUserInfo(User user) { return jdbcTemplate.queryForObject("SELECT userinfo FROM users where id=?", String.class, user.getId()); } /** * Получить информацию о пользователе * @param user пользователь * @return информация */ public UserInfo getUserInfoClass(User user) { return jdbcTemplate.queryForObject("SELECT url, town, lastlogin, regdate FROM users WHERE id=?", (resultSet, i) -> new UserInfo(resultSet), user.getId()); } /** * Получить информацию о бане * @param user пользователь * @return информация о бане :-) */ public BanInfo getBanInfoClass(User user) { List<BanInfo> infoList = jdbcTemplate.query(queryBanInfoClass, (resultSet, i) -> { Timestamp date = resultSet.getTimestamp("bandate"); String reason = resultSet.getString("reason"); User moderator = getUser(resultSet.getInt("ban_by")); return new BanInfo(date, reason, moderator); }, user.getId()); if (infoList.isEmpty()) { return null; } else { return infoList.get(0); } } public int getExactCommentCount(User user) { try { return jdbcTemplate.queryForObject(queryCommentStat, Integer.class, user.getId()); } catch (EmptyResultDataAccessException exception) { return 0; } } public Tuple2<Timestamp, Timestamp> getFirstAndLastCommentDate(User user) { return jdbcTemplate.queryForObject(queryCommentDates, (resultSet, i) -> new Tuple2<>(resultSet.getTimestamp("first"), resultSet.getTimestamp("last")), user.getId()); } /** * Получить список новых пользователей зарегистрирововавшихся за последние 3(три) дня * @return список новых пользователей */ public List<Integer> getNewUserIds() { return jdbcTemplate.queryForList(queryNewUsers, Integer.class); } @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void removeUserInfo(User user, User moderator) { String userInfo = getUserInfo(user); if(userInfo == null || userInfo.trim().isEmpty()) { return; } setUserInfo(user.getId(), null); changeScore(user.getId(), -10); userLogDao.logResetInfo(user, moderator, userInfo, -10); } /** * Отчистка userpicture пользователя, с обрезанием шкворца если удаляет модератор * @param user пользовтель у которого чистят * @param cleaner пользователь который чистит */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public boolean resetUserpic(User user, User cleaner) { boolean r; if (user.getPhoto()==null) { r = jdbcTemplate.update("UPDATE users SET photo='' WHERE id=? and photo is null", user.getId()) > 0; } else { r = jdbcTemplate.update("UPDATE users SET photo=null WHERE id=? and photo is not null", user.getId()) > 0; } if (!r) { return false; } // Обрезать score у пользователя если его чистит модератор и пользователь не модератор if(cleaner.isModerator() && cleaner.getId() != user.getId() && !user.isModerator()) { changeScore(user.getId(), -10); userLogDao.logResetUserpic(user, cleaner, -10); } else { userLogDao.logResetUserpic(user, cleaner, 0); } return r; } /** * Обновление userpic-а пользовтаеля * @param user пользователь * @param photo userpick */ @CacheEvict(value="Users", key="#user.id") @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void setPhoto(@Nonnull User user, @Nonnull String photo){ jdbcTemplate.update("UPDATE users SET photo=? WHERE id=?", photo, user.getId()); userLogDao.logSetUserpic(user, photo); } /** * Обновление дополнительной информации пользователя * @param userid пользователь * @param text текст дополнительной информации */ private void setUserInfo(int userid, String text){ jdbcTemplate.update("UPDATE users SET userinfo=? where id=?", text, userid); } /** * Изменение шкворца пользовтаеля, принимает отрицательные и положительные значения * не накладывает никаких ограничений на параметры используется в купэ с другими * методами и не является транзакцией * @param id id пользователя * @param delta дельта на которую меняется шкворец */ @CacheEvict(value="Users", key="#id") public void changeScore(int id, int delta) { if (jdbcTemplate.update(queryChangeScore, delta, id)==0) { throw new IllegalArgumentException(new UserNotFoundException(id)); } } /** * Смена признака корректора для пользователя * @param user пользователь у которого меняется признак корректора */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void toggleCorrector(User user, User moderator) { if(user.canCorrect()){ jdbcTemplate.update("UPDATE users SET corrector='f' WHERE id=?", user.getId()); userLogDao.unsetCorrector(user, moderator); } else { jdbcTemplate.update("UPDATE users SET corrector='t' WHERE id=?", user.getId()); userLogDao.setCorrector(user, moderator); } } /** * Смена стиля\темы пользователя * @param user пользователь у которого меняется стиль\тема */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void setStyle(User user, String theme){ jdbcTemplate.update(updateUserStyle, theme, user.getId()); } /** * Сброс пороля на случайный * @param user пользователь которому сбрасывается пароль * @return новый пароь в открытом виде */ @CacheEvict(value="Users", key="#user.id") @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public String resetPassword(User user){ String password = StringUtil.generatePassword(); userLogDao.logResetPassword(user, user); return setPassword(user, password); } @CacheEvict(value="Users", key="#user.id") @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void resetPassword(User user, User moderator){ setPassword(user, StringUtil.generatePassword()); userLogDao.logResetPassword(user, moderator); } private String setPassword(User user, String password) { PasswordEncryptor encryptor = new BasicPasswordEncryptor(); String encryptedPassword = encryptor.encryptPassword(password); jdbcTemplate.update("UPDATE users SET passwd=?, lostpwd = 'epoch' WHERE id=?", encryptedPassword, user.getId()); return password; } public void updateResetDate(User user, Timestamp now) { jdbcTemplate.update("UPDATE users SET lostpwd=? WHERE id=?", now, user.getId()); } public Timestamp getResetDate(User user) { return jdbcTemplate.queryForObject("SELECT lostpwd FROM users WHERE id=?", Timestamp.class, user.getId()); } /** * Блокировка пользователя * @param user блокируемый пользователь * @param moderator модератор который блокирует пользователя * @param reason причина блокировки */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void block(@Nonnull User user, @Nonnull User moderator, @Nonnull String reason) { jdbcTemplate.update("UPDATE users SET blocked='t' WHERE id=?", user.getId()); jdbcTemplate.update("INSERT INTO ban_info (userid, reason, ban_by) VALUES (?, ?, ?)", user.getId(), reason, moderator.getId()); userLogDao.logBlockUser(user, moderator, reason); } /** * Ставим score=50 если он меньше * * @param user кому ставим score * @param moderator модератор */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void score50(@Nonnull User user, @Nonnull User moderator) { if (jdbcTemplate.update("UPDATE users SET score=GREATEST(score, 50), max_score=GREATEST(max_score, 50) WHERE id=? AND score<50", user.getId()) > 0) { userLogDao.logScore50(user, moderator); } } /** * Разблокировка пользователя * @param user разблокируемый пользователь */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void unblock(@Nonnull User user, @Nonnull User moderator){ jdbcTemplate.update("UPDATE users SET blocked='f' WHERE id=?", user.getId()); jdbcTemplate.update("DELETE FROM ban_info WHERE userid=?", user.getId()); userLogDao.logUnblockUser(user, moderator); } public List<Integer> getModeratorIds() { return jdbcTemplate.queryForList("SELECT id FROM users WHERE canmod ORDER BY id", Integer.class); } public List<Integer> getCorrectorIds() { return jdbcTemplate.queryForList("SELECT id FROM users WHERE corrector ORDER BY id", Integer.class); } public User getByEmail(String email, boolean searchBlocked) { try { int id; if (searchBlocked) { id = jdbcTemplate.queryForObject( "SELECT id FROM users WHERE email=? ORDER BY blocked ASC, id DESC LIMIT 1", Integer.class, email.toLowerCase() ); } else { id = jdbcTemplate.queryForObject( "SELECT id FROM users WHERE email=? AND NOT blocked ORDER BY id DESC LIMIT 1", Integer.class, email.toLowerCase() ); } return getUser(id); } catch (EmptyResultDataAccessException ex) { return null; } } public boolean canResetPassword(User user) { return !jdbcTemplate.queryForObject( "SELECT lostpwd>CURRENT_TIMESTAMP-'1 week'::interval as datecheck FROM users WHERE id=?", Boolean.class, user.getId() ); } @CacheEvict(value="Users", key="#user.id") public void activateUser(User user) { jdbcTemplate.update("UPDATE users SET activated='t' WHERE id=?", user.getId()); } @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @CacheEvict(value="Users", key="#user.id") public void updateUser( @Nonnull User user, String name, String url, @Nullable String newEmail, String town, @Nullable String password, String info ) { jdbcTemplate.update("UPDATE users SET name=?, url=?, town=? WHERE id=?", name, url, town, user.getId()); if (newEmail!=null) { jdbcTemplate.update("UPDATE users SET new_email=? WHERE id=?", newEmail, user.getId()); } if (password != null) { setPassword(user, password); userLogDao.logSetPassword(user); } setUserInfo(user.getId(), info); } @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public int createUser( String name, String nick, String password, String url, InternetAddress mail, String town, String ip ) { PasswordEncryptor encryptor = new BasicPasswordEncryptor(); int userid = jdbcTemplate.queryForObject("select nextval('s_uid') as userid", Integer.class); jdbcTemplate.update( "INSERT INTO users " + "(id, name, nick, passwd, url, email, town, score, max_score,regdate) " + "VALUES (?,?,?,?,?,?,?,45,45,current_timestamp)", userid, name, nick, encryptor.encryptPassword(password), url==null?null: URLUtil.fixURL(url), mail.getAddress(), town ); userLogDao.logRegister(userid, ip); return userid; } public boolean isUserExists(String nick) { int c = jdbcTemplate.queryForObject("SELECT count(*) as c FROM users WHERE nick=?", Integer.class, nick); return c>0; } public boolean hasSimilarUsers(String nick) { int c = jdbcTemplate.queryForObject("SELECT count(*) FROM users WHERE " + "NOT blocked AND score>=200 AND lastlogin>CURRENT_TIMESTAMP-'3 years'::INTERVAL " + "AND levenshtein_less_equal(lower(nick), ?, 1)<=1", Integer.class, nick.toLowerCase()); return c>0; } public String getNewEmail(@Nonnull User user) { return jdbcTemplate.queryForObject("SELECT new_email FROM users WHERE id=?", String.class, user.getId()); } @CacheEvict(value="Users", key="#user.id") @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void acceptNewEmail(@Nonnull User user, @Nonnull String newEmail) { jdbcTemplate.update("UPDATE users SET email=?, new_email=null WHERE id=?", newEmail, user.getId()); userLogDao.logAcceptNewEmail(user, newEmail); } /** * Update lastlogin time in database * @param user logged user * @throws SQLException on database failure */ public void updateLastlogin(User user, boolean force) { if (force) { jdbcTemplate.update("UPDATE users SET lastlogin=CURRENT_TIMESTAMP WHERE id=?", user.getId()); } else { jdbcTemplate.update("UPDATE users SET lastlogin=CURRENT_TIMESTAMP WHERE id=? AND CURRENT_TIMESTAMP-lastlogin > '1 hour'::interval", user.getId()); } } }