| 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 } |