--- a/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt Fri Mar 14 08:09:05 2025 +0100 +++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt Sat May 17 17:39:48 2025 +0200 @@ -28,6 +28,7 @@ import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME import de.uapcore.lightpit.dao.DataAccessObject import de.uapcore.lightpit.dao.createDataAccessObject +import de.uapcore.lightpit.entities.User import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -35,6 +36,7 @@ import java.sql.SQLException import java.time.ZoneId import java.util.* +import java.sql.Date as SqlDate abstract class AbstractServlet : HttpServlet() { @@ -89,8 +91,10 @@ ) { val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) val method = mapping.second + val authenticatedUser = req.remoteUser?.let(dao::findUserByName) + showWhatsNewPopup(authenticatedUser, req, dao) logger.trace("invoke {0}", method) - method(HttpRequest(req, resp, params), dao) + method(HttpRequest(authenticatedUser, req, resp, params), dao) } private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" @@ -124,9 +128,7 @@ req.characterEncoding = "UTF-8" // set some internal request attributes - val http = HttpRequest(req, resp) val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") - req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) req.getHeader("Referer")?.let { // TODO: add a sanity check to avoid link injection @@ -136,7 +138,7 @@ // choose the requested language as session language (if available) if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { // language selection stored in cookie - val cookieLocale = cookieLanguage(http) + val cookieLocale = cookieLanguage(req) // if no cookie, fall back to request locale a.k.a "Browser Language" val reqLocale = cookieLocale ?: req.locale @@ -145,7 +147,7 @@ val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() // select the language (this will also refresh the cookie max-age) - selectLanguage(http, sessionLocale) + selectLanguage(req, resp, sessionLocale) logger.debug( "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage @@ -159,17 +161,18 @@ // determine the timezone if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) { // timezone selection stored in cookie - val cookieTimezone = cookieTimezone(http) + val cookieTimezone = cookieTimezone(req) // if no cookie, fall back to server's timezone (the browser does not transmit one) val timezone = cookieTimezone ?: ZoneId.systemDefault() - selectTimezone(http, timezone) + selectTimezone(req, resp, timezone) logger.debug("Timezone for session {0} set to {1}", session.id, timezone) } // if this is an error path, bypass the normal flow if (fullPath.startsWith("/error/")) { + val http = HttpRequest(null, req, resp) http.styleSheets = listOf("error") http.render("error") return @@ -210,6 +213,16 @@ } } + private fun showWhatsNewPopup(user: User?, req: HttpServletRequest, dao: DataAccessObject) { + if (user == null) return + logger.trace("show user with ID {0} what's new", user.id) + val userKnowsUpdatesUntil = dao.untilWhenUserKnowsUpdates(user) + if (userKnowsUpdatesUntil == null || userKnowsUpdatesUntil.before(SqlDate.valueOf(Constants.VERSION_DATE))) { + dao.updateUserKnowsUpdates(user) + req.setAttribute("showWhatsNew", true) + } + } + override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { doProcess(req, resp, getMappings) } @@ -224,40 +237,48 @@ return locales.ifEmpty { listOf(Locale.ENGLISH) } } - private fun cookieLanguage(http: HttpRequest): Locale? = - http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } + private fun cookieLanguage(request: HttpServletRequest): Locale? = + request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale - private fun cookieTimezone(http: HttpRequest): ZoneId? = - http.request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } + private fun cookieTimezone(request: HttpServletRequest): ZoneId? = + request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } ?.runCatching { ZoneId.of(this.value)}?.getOrNull() protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) { - http.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) + selectTimezone(http.request, http.response, zoneId) + } + + private fun selectTimezone(request: HttpServletRequest, response: HttpServletResponse, zoneId: ZoneId) { + request.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id) cookie.isHttpOnly = true - cookie.path = http.request.contextPath + cookie.path = request.contextPath cookie.maxAge = COOKIE_MAX_AGE - http.response.addCookie(cookie) + response.addCookie(cookie) } protected fun selectLanguage(http: HttpRequest, locale: Locale) { - http.response.locale = locale - http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) + selectLanguage(http.request, http.response, locale) + } + + private fun selectLanguage(request: HttpServletRequest, response: HttpServletResponse, locale: Locale) { + response.locale = locale + request.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) // delete cookie if language selection matches request locale, otherwise set cookie val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") cookie.isHttpOnly = true - cookie.path = http.request.contextPath - if (http.request.locale.language == locale.language) { + cookie.path = request.contextPath + if (request.locale.language == locale.language) { cookie.maxAge = 0 } else { cookie.value = locale.language cookie.maxAge = COOKIE_MAX_AGE } - http.response.addCookie(cookie) + response.addCookie(cookie) } }