26 package de.uapcore.lightpit |
26 package de.uapcore.lightpit |
27 |
27 |
28 import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME |
28 import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME |
29 import de.uapcore.lightpit.dao.DataAccessObject |
29 import de.uapcore.lightpit.dao.DataAccessObject |
30 import de.uapcore.lightpit.dao.createDataAccessObject |
30 import de.uapcore.lightpit.dao.createDataAccessObject |
|
31 import de.uapcore.lightpit.entities.User |
31 import jakarta.servlet.http.Cookie |
32 import jakarta.servlet.http.Cookie |
32 import jakarta.servlet.http.HttpServlet |
33 import jakarta.servlet.http.HttpServlet |
33 import jakarta.servlet.http.HttpServletRequest |
34 import jakarta.servlet.http.HttpServletRequest |
34 import jakarta.servlet.http.HttpServletResponse |
35 import jakarta.servlet.http.HttpServletResponse |
35 import java.sql.SQLException |
36 import java.sql.SQLException |
36 import java.time.ZoneId |
37 import java.time.ZoneId |
37 import java.util.* |
38 import java.util.* |
|
39 import java.sql.Date as SqlDate |
38 |
40 |
39 abstract class AbstractServlet : HttpServlet() { |
41 abstract class AbstractServlet : HttpServlet() { |
40 |
42 |
41 companion object { |
43 companion object { |
42 const val COOKIE_MAX_AGE = 2592000 // 30 days |
44 const val COOKIE_MAX_AGE = 2592000 // 30 days |
87 resp: HttpServletResponse, |
89 resp: HttpServletResponse, |
88 dao: DataAccessObject |
90 dao: DataAccessObject |
89 ) { |
91 ) { |
90 val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) |
92 val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req)) |
91 val method = mapping.second |
93 val method = mapping.second |
|
94 val authenticatedUser = req.remoteUser?.let(dao::findUserByName) |
|
95 showWhatsNewPopup(authenticatedUser, req, dao) |
92 logger.trace("invoke {0}", method) |
96 logger.trace("invoke {0}", method) |
93 method(HttpRequest(req, resp, params), dao) |
97 method(HttpRequest(authenticatedUser, req, resp, params), dao) |
94 } |
98 } |
95 |
99 |
96 private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" |
100 private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/" |
97 |
101 |
98 protected fun sanitizeJson(str: String): String { |
102 protected fun sanitizeJson(str: String): String { |
122 |
126 |
123 // the very first thing to do is to force UTF-8 |
127 // the very first thing to do is to force UTF-8 |
124 req.characterEncoding = "UTF-8" |
128 req.characterEncoding = "UTF-8" |
125 |
129 |
126 // set some internal request attributes |
130 // set some internal request attributes |
127 val http = HttpRequest(req, resp) |
|
128 val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") |
131 val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("") |
129 req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref) |
|
130 req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) |
132 req.setAttribute(Constants.REQ_ATTR_PATH, fullPath) |
131 req.getHeader("Referer")?.let { |
133 req.getHeader("Referer")?.let { |
132 // TODO: add a sanity check to avoid link injection |
134 // TODO: add a sanity check to avoid link injection |
133 req.setAttribute(Constants.REQ_ATTR_REFERER, it) |
135 req.setAttribute(Constants.REQ_ATTR_REFERER, it) |
134 } |
136 } |
135 |
137 |
136 // choose the requested language as session language (if available) |
138 // choose the requested language as session language (if available) |
137 if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { |
139 if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) { |
138 // language selection stored in cookie |
140 // language selection stored in cookie |
139 val cookieLocale = cookieLanguage(http) |
141 val cookieLocale = cookieLanguage(req) |
140 |
142 |
141 // if no cookie, fall back to request locale a.k.a "Browser Language" |
143 // if no cookie, fall back to request locale a.k.a "Browser Language" |
142 val reqLocale = cookieLocale ?: req.locale |
144 val reqLocale = cookieLocale ?: req.locale |
143 |
145 |
144 val availableLanguages = availableLanguages() |
146 val availableLanguages = availableLanguages() |
145 val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() |
147 val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first() |
146 |
148 |
147 // select the language (this will also refresh the cookie max-age) |
149 // select the language (this will also refresh the cookie max-age) |
148 selectLanguage(http, sessionLocale) |
150 selectLanguage(req, resp, sessionLocale) |
149 |
151 |
150 logger.debug( |
152 logger.debug( |
151 "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage |
153 "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage |
152 ) |
154 ) |
153 } else { |
155 } else { |
157 } |
159 } |
158 |
160 |
159 // determine the timezone |
161 // determine the timezone |
160 if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) { |
162 if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) { |
161 // timezone selection stored in cookie |
163 // timezone selection stored in cookie |
162 val cookieTimezone = cookieTimezone(http) |
164 val cookieTimezone = cookieTimezone(req) |
163 |
165 |
164 // if no cookie, fall back to server's timezone (the browser does not transmit one) |
166 // if no cookie, fall back to server's timezone (the browser does not transmit one) |
165 val timezone = cookieTimezone ?: ZoneId.systemDefault() |
167 val timezone = cookieTimezone ?: ZoneId.systemDefault() |
166 |
168 |
167 selectTimezone(http, timezone) |
169 selectTimezone(req, resp, timezone) |
168 logger.debug("Timezone for session {0} set to {1}", session.id, timezone) |
170 logger.debug("Timezone for session {0} set to {1}", session.id, timezone) |
169 } |
171 } |
170 |
172 |
171 // if this is an error path, bypass the normal flow |
173 // if this is an error path, bypass the normal flow |
172 if (fullPath.startsWith("/error/")) { |
174 if (fullPath.startsWith("/error/")) { |
|
175 val http = HttpRequest(null, req, resp) |
173 http.styleSheets = listOf("error") |
176 http.styleSheets = listOf("error") |
174 http.render("error") |
177 http.render("error") |
175 return |
178 return |
176 } |
179 } |
177 |
180 |
208 logger.debug("Details: ", ex) |
211 logger.debug("Details: ", ex) |
209 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode) |
212 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.errorCode) |
210 } |
213 } |
211 } |
214 } |
212 |
215 |
|
216 private fun showWhatsNewPopup(user: User?, req: HttpServletRequest, dao: DataAccessObject) { |
|
217 if (user == null) return |
|
218 logger.trace("show user with ID {0} what's new", user.id) |
|
219 val userKnowsUpdatesUntil = dao.untilWhenUserKnowsUpdates(user) |
|
220 if (userKnowsUpdatesUntil == null || userKnowsUpdatesUntil.before(SqlDate.valueOf(Constants.VERSION_DATE))) { |
|
221 dao.updateUserKnowsUpdates(user) |
|
222 req.setAttribute("showWhatsNew", true) |
|
223 } |
|
224 } |
|
225 |
213 override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { |
226 override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { |
214 doProcess(req, resp, getMappings) |
227 doProcess(req, resp, getMappings) |
215 } |
228 } |
216 |
229 |
217 override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { |
230 override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { |
222 val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList() |
235 val langTags = servletContext.getInitParameter(Constants.CTX_ATTR_LANGUAGES)?.split(",")?.map(String::trim) ?: emptyList() |
223 val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() } |
236 val locales = langTags.map(Locale::forLanguageTag).filter { it.language.isNotEmpty() } |
224 return locales.ifEmpty { listOf(Locale.ENGLISH) } |
237 return locales.ifEmpty { listOf(Locale.ENGLISH) } |
225 } |
238 } |
226 |
239 |
227 private fun cookieLanguage(http: HttpRequest): Locale? = |
240 private fun cookieLanguage(request: HttpServletRequest): Locale? = |
228 http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } |
241 request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME } |
229 ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() |
242 ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull() |
230 |
243 |
231 protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale |
244 protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale |
232 |
245 |
233 private fun cookieTimezone(http: HttpRequest): ZoneId? = |
246 private fun cookieTimezone(request: HttpServletRequest): ZoneId? = |
234 http.request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } |
247 request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME } |
235 ?.runCatching { ZoneId.of(this.value)}?.getOrNull() |
248 ?.runCatching { ZoneId.of(this.value)}?.getOrNull() |
236 |
249 |
237 protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String |
250 protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String |
238 |
251 |
239 protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) { |
252 protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) { |
240 http.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) |
253 selectTimezone(http.request, http.response, zoneId) |
|
254 } |
|
255 |
|
256 private fun selectTimezone(request: HttpServletRequest, response: HttpServletResponse, zoneId: ZoneId) { |
|
257 request.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id) |
241 val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id) |
258 val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id) |
242 cookie.isHttpOnly = true |
259 cookie.isHttpOnly = true |
243 cookie.path = http.request.contextPath |
260 cookie.path = request.contextPath |
244 cookie.maxAge = COOKIE_MAX_AGE |
261 cookie.maxAge = COOKIE_MAX_AGE |
245 http.response.addCookie(cookie) |
262 response.addCookie(cookie) |
246 } |
263 } |
247 |
264 |
248 protected fun selectLanguage(http: HttpRequest, locale: Locale) { |
265 protected fun selectLanguage(http: HttpRequest, locale: Locale) { |
249 http.response.locale = locale |
266 selectLanguage(http.request, http.response, locale) |
250 http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) |
267 } |
|
268 |
|
269 private fun selectLanguage(request: HttpServletRequest, response: HttpServletResponse, locale: Locale) { |
|
270 response.locale = locale |
|
271 request.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale) |
251 // delete cookie if language selection matches request locale, otherwise set cookie |
272 // delete cookie if language selection matches request locale, otherwise set cookie |
252 val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") |
273 val cookie = Cookie(LANGUAGE_COOKIE_NAME, "") |
253 cookie.isHttpOnly = true |
274 cookie.isHttpOnly = true |
254 cookie.path = http.request.contextPath |
275 cookie.path = request.contextPath |
255 if (http.request.locale.language == locale.language) { |
276 if (request.locale.language == locale.language) { |
256 cookie.maxAge = 0 |
277 cookie.maxAge = 0 |
257 } else { |
278 } else { |
258 cookie.value = locale.language |
279 cookie.value = locale.language |
259 cookie.maxAge = COOKIE_MAX_AGE |
280 cookie.maxAge = COOKIE_MAX_AGE |
260 } |
281 } |
261 http.response.addCookie(cookie) |
282 response.addCookie(cookie) |
262 } |
283 } |
263 } |
284 } |