/* * This file is part of ARSnova Backend. * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ARSnova Backend is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.thm.arsnova.services; import de.thm.arsnova.ImageUtils; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.model.Course; import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.domain.ILearningProgressFactory; import de.thm.arsnova.domain.LearningProgress; import de.thm.arsnova.entities.LearningProgressOptions; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.SessionFeature; import de.thm.arsnova.entities.SessionInfo; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.ImportExportSession; import de.thm.arsnova.entities.transport.LearningProgressValues; import de.thm.arsnova.events.DeleteSessionEvent; import de.thm.arsnova.events.FeatureChangeEvent; import de.thm.arsnova.events.FlipFlashcardsEvent; import de.thm.arsnova.events.LockFeedbackEvent; import de.thm.arsnova.events.NewSessionEvent; import de.thm.arsnova.events.StatusSessionEvent; import de.thm.arsnova.exceptions.BadRequestException; import de.thm.arsnova.exceptions.ForbiddenException; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.PayloadTooLargeException; import de.thm.arsnova.exceptions.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import java.io.Serializable; import java.util.Comparator; import java.util.List; import java.util.UUID; /** * Performs all session related operations. */ @Service public class SessionService implements ISessionService, ApplicationEventPublisherAware { public static class SessionNameComparator implements Comparator<Session>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(final Session session1, final Session session2) { return session1.getName().compareToIgnoreCase(session2.getName()); } } public static class SessionInfoNameComparator implements Comparator<SessionInfo>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(final SessionInfo session1, final SessionInfo session2) { return session1.getName().compareToIgnoreCase(session2.getName()); } } public static class SessionShortNameComparator implements Comparator<Session>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(final Session session1, final Session session2) { return session1.getShortName().compareToIgnoreCase(session2.getShortName()); } } public static class SessionInfoShortNameComparator implements Comparator<SessionInfo>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(final SessionInfo session1, final SessionInfo session2) { return session1.getShortName().compareToIgnoreCase(session2.getShortName()); } } private static final long SESSION_INACTIVITY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; @Autowired private IDatabaseDao databaseDao; @Autowired private IUserService userService; @Autowired private IFeedbackService feedbackService; @Autowired private ILearningProgressFactory learningProgressFactory; @Autowired(required = false) private ConnectorClient connectorClient; @Autowired private ImageUtils imageUtils; @Value("${session.guest-session.cleanup-days:0}") private int guestSessionInactivityThresholdDays; @Value("${pp.logofilesize_b}") private int uploadFileSizeByte; private ApplicationEventPublisher publisher; private static final Logger logger = LoggerFactory.getLogger(SessionService.class); @Scheduled(fixedDelay = SESSION_INACTIVITY_CHECK_INTERVAL_MS) public void deleteInactiveSessions() { if (guestSessionInactivityThresholdDays > 0) { logger.info("Delete inactive sessions."); long unixTime = System.currentTimeMillis(); long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L; databaseDao.deleteInactiveGuestSessions(lastActivityBefore); } } @Scheduled(fixedDelay = SESSION_INACTIVITY_CHECK_INTERVAL_MS) public void deleteInactiveVisitedSessionLists() { if (guestSessionInactivityThresholdDays > 0) { logger.info("Delete lists of visited session for inactive users."); long unixTime = System.currentTimeMillis(); long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L; databaseDao.deleteInactiveGuestVisitedSessionLists(lastActivityBefore); } } public void setDatabaseDao(final IDatabaseDao newDatabaseDao) { databaseDao = newDatabaseDao; } @Override public Session joinSession(final String keyword, final UUID socketId) { /* Socket.IO solution */ Session session = null != keyword ? databaseDao.getSessionFromKeyword(keyword) : null; if (null == session) { userService.removeUserFromSessionBySocketId(socketId); return null; } final User user = userService.getUser2SocketId(socketId); userService.addUserToSessionBySocketId(socketId, keyword); if (session.getCreator().equals(user.getUsername())) { databaseDao.updateSessionOwnerActivity(session); } databaseDao.registerAsOnlineUser(user, session); if (connectorClient != null && session.isCourseSession()) { final String courseid = session.getCourseId(); if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { throw new ForbiddenException("User is no course member."); } } return session; } @Override @PreAuthorize("isAuthenticated()") public Session getSession(final String keyword) { final User user = userService.getCurrentUser(); return Session.anonymizedCopy(this.getSessionInternal(keyword, user)); } @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public Session getSessionForAdmin(final String keyword) { return databaseDao.getSessionFromKeyword(keyword); } /* * The "internal" suffix means it is called by internal services that have no authentication! * TODO: Find a better way of doing this... */ @Override public Session getSessionInternal(final String keyword, final User user) { final Session session = databaseDao.getSessionFromKeyword(keyword); if (session == null) { throw new NotFoundException(); } if (!session.isActive()) { if (user.hasRole(UserSessionService.Role.STUDENT)) { throw new ForbiddenException("User is not session creator."); } else if (user.hasRole(UserSessionService.Role.SPEAKER) && !session.isCreator(user)) { throw new ForbiddenException("User is not session creator."); } } if (connectorClient != null && session.isCourseSession()) { final String courseid = session.getCourseId(); if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { throw new ForbiddenException("User is no course member."); } } return session; } @Override @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public List<Session> getUserSessions(String username) { return databaseDao.getSessionsForUsername(username, 0, 0); } @Override @PreAuthorize("isAuthenticated()") public List<Session> getMySessions(final int offset, final int limit) { return databaseDao.getMySessions(userService.getCurrentUser(), offset, limit); } @Override @PreAuthorize("isAuthenticated()") public List<SessionInfo> getPublicPoolSessionsInfo() { return databaseDao.getPublicPoolSessionsInfo(); } @Override @PreAuthorize("isAuthenticated()") public List<SessionInfo> getMyPublicPoolSessionsInfo() { return databaseDao.getMyPublicPoolSessionsInfo(userService.getCurrentUser()); } @Override @PreAuthorize("isAuthenticated()") public List<SessionInfo> getMySessionsInfo(final int offset, final int limit) { final User user = userService.getCurrentUser(); return databaseDao.getMySessionsInfo(user, offset, limit); } @Override @PreAuthorize("isAuthenticated()") public List<Session> getMyVisitedSessions(final int offset, final int limit) { return databaseDao.getMyVisitedSessions(userService.getCurrentUser(), offset, limit); } @Override @PreAuthorize("isAuthenticated() and hasPermission(1, 'motd', 'admin')") public List<Session> getUserVisitedSessions(String username) { return databaseDao.getVisitedSessionsForUsername(username, 0, 0); } @Override @PreAuthorize("isAuthenticated()") public List<SessionInfo> getMyVisitedSessionsInfo(final int offset, final int limit) { return databaseDao.getMyVisitedSessionsInfo(userService.getCurrentUser(), offset, limit); } @Override @PreAuthorize("isAuthenticated()") public Session saveSession(final Session session) { if (connectorClient != null && session.getCourseId() != null) { if (!connectorClient.getMembership( userService.getCurrentUser().getUsername(), session.getCourseId()).isMember() ) { throw new ForbiddenException(); } } handleLogo(session); // set some default values LearningProgressOptions lpo = new LearningProgressOptions(); lpo.setType("questions"); session.setLearningProgressOptions(lpo); SessionFeature sf = new SessionFeature(); sf.setLecture(true); sf.setFeedback(true); sf.setInterposed(true); sf.setJitt(true); sf.setLearningProgress(true); sf.setPi(true); session.setFeatures(sf); final Session result = databaseDao.saveSession(userService.getCurrentUser(), session); this.publisher.publishEvent(new NewSessionEvent(this, result)); return result; } @Override public boolean sessionKeyAvailable(final String keyword) { return databaseDao.sessionKeyAvailable(keyword); } @Override public String generateKeyword() { final int low = 10000000; final int high = 100000000; final String keyword = String .valueOf((int) (Math.random() * (high - low) + low)); if (sessionKeyAvailable(keyword)) { return keyword; } return generateKeyword(); } @Override public int countSessions(final List<Course> courses) { final List<Session> sessions = databaseDao.getCourseSessions(courses); if (sessions == null) { return 0; } return sessions.size(); } @Override public int activeUsers(final String sessionkey) { return userService.getUsersInSession(sessionkey).size(); } @Override public Session setActive(final String sessionkey, final Boolean lock) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); final User user = userService.getCurrentUser(); if (!session.isCreator(user)) { throw new ForbiddenException("User is not session creator."); } session.setActive(lock); this.publisher.publishEvent(new StatusSessionEvent(this, session)); return databaseDao.updateSession(session); } @Override @PreAuthorize("isAuthenticated() and hasPermission(#session, 'owner')") public Session updateSession(final String sessionkey, final Session session) { final Session existingSession = databaseDao.getSessionFromKeyword(sessionkey); existingSession.setActive(session.isActive()); existingSession.setShortName(session.getShortName()); existingSession.setPpAuthorName(session.getPpAuthorName()); existingSession.setPpAuthorMail(session.getPpAuthorMail()); existingSession.setShortName(session.getShortName()); existingSession.setPpAuthorName(session.getPpAuthorName()); existingSession.setPpFaculty(session.getPpFaculty()); existingSession.setName(session.getName()); existingSession.setPpUniversity(session.getPpUniversity()); existingSession.setPpDescription(session.getPpDescription()); existingSession.setPpLevel(session.getPpLevel()); existingSession.setPpLicense(session.getPpLicense()); existingSession.setPpSubject(session.getPpSubject()); existingSession.setFeedbackLock(session.getFeedbackLock()); handleLogo(session); existingSession.setPpLogo(session.getPpLogo()); return databaseDao.updateSession(existingSession); } @Override @PreAuthorize("isAuthenticated() and hasPermission(1,'motd','admin')") public Session changeSessionCreator(String sessionkey, String newCreator) { final Session existingSession = databaseDao.getSessionFromKeyword(sessionkey); if (existingSession == null) { throw new NullPointerException("Could not load session " + sessionkey + "."); } return databaseDao.changeSessionCreator(existingSession, newCreator); } /* * The "internal" suffix means it is called by internal services that have no authentication! * TODO: Find a better way of doing this... */ @Override public Session updateSessionInternal(final Session session, final User user) { if (session.isCreator(user)) { return databaseDao.updateSession(session); } return null; } @Override @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public void deleteSession(final String sessionkey) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); databaseDao.deleteSession(session); this.publisher.publishEvent(new DeleteSessionEvent(this, session)); } @Override @PreAuthorize("isAuthenticated()") public LearningProgressValues getLearningProgress(final String sessionkey, final String progressType, final String questionVariant) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); LearningProgress learningProgress = learningProgressFactory.create(progressType, questionVariant); return learningProgress.getCourseProgress(session); } @Override @PreAuthorize("isAuthenticated()") public LearningProgressValues getMyLearningProgress(final String sessionkey, final String progressType, final String questionVariant) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); final User user = userService.getCurrentUser(); LearningProgress learningProgress = learningProgressFactory.create(progressType, questionVariant); return learningProgress.getMyProgress(session, user); } @Override @PreAuthorize("isAuthenticated()") public SessionInfo importSession(ImportExportSession importSession) { final User user = userService.getCurrentUser(); final SessionInfo info = databaseDao.importSession(user, importSession); if (info == null) { throw new NullPointerException("Could not import session."); } return info; } @Override @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public ImportExportSession exportSession(String sessionkey, Boolean withAnswerStatistics, Boolean withFeedbackQuestions) { return databaseDao.exportSession(sessionkey, withAnswerStatistics, withFeedbackQuestions); } @Override @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") public SessionInfo copySessionToPublicPool(String sessionkey, de.thm.arsnova.entities.transport.ImportExportSession.PublicPool pp) { ImportExportSession temp = databaseDao.exportSession(sessionkey, false, false); temp.getSession().setPublicPool(pp); temp.getSession().setSessionType("public_pool"); final User user = userService.getCurrentUser(); return databaseDao.importSession(user, temp); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } @Override public SessionFeature getSessionFeatures(String sessionkey) { return databaseDao.getSessionFromKeyword(sessionkey).getFeatures(); } @Override public SessionFeature changeSessionFeatures(String sessionkey, SessionFeature features) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); final User user = userService.getCurrentUser(); if (!session.isCreator(user)) { throw new UnauthorizedException("User is not session creator."); } session.setFeatures(features); this.publisher.publishEvent(new FeatureChangeEvent(this, session)); return databaseDao.updateSession(session).getFeatures(); } @Override public boolean lockFeedbackInput(String sessionkey, Boolean lock) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); final User user = userService.getCurrentUser(); if (!session.isCreator(user)) { throw new UnauthorizedException("User is not session creator."); } if (!lock) { feedbackService.cleanFeedbackVotesInSession(sessionkey, 0); } session.setFeedbackLock(lock); this.publisher.publishEvent(new LockFeedbackEvent(this, session)); return databaseDao.updateSession(session).getFeedbackLock(); } @Override public boolean flipFlashcards(String sessionkey, Boolean flip) { final Session session = databaseDao.getSessionFromKeyword(sessionkey); final User user = userService.getCurrentUser(); if (!session.isCreator(user)) { throw new UnauthorizedException("User is not session creator."); } session.setFlipFlashcards(flip); this.publisher.publishEvent(new FlipFlashcardsEvent(this, session)); return databaseDao.updateSession(session).getFlipFlashcards(); } private void handleLogo(Session session) { if (session.getPpLogo() != null) { if (session.getPpLogo().startsWith("http")) { final String base64ImageString = imageUtils.encodeImageToString(session.getPpLogo()); if (base64ImageString == null) { throw new BadRequestException("Could not encode image."); } session.setPpLogo(base64ImageString); } // base64 adds offset to filesize, formula taken from: http://en.wikipedia.org/wiki/Base64#MIME final int fileSize = (int) ((session.getPpLogo().length() - 814) / 1.37); if (fileSize > uploadFileSizeByte) { throw new PayloadTooLargeException("Could not save file. File is too large with " + fileSize + " Byte."); } } } }