Tue, 03 Aug 2021 13:41:32 +0200
#21 adds input validation mechanism
Also upgrades to Kotlin 1.5.21
1.1 --- a/build.gradle.kts Tue Aug 03 12:22:10 2021 +0200 1.2 +++ b/build.gradle.kts Tue Aug 03 13:41:32 2021 +0200 1.3 @@ -1,7 +1,7 @@ 1.4 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 1.5 1.6 plugins { 1.7 - kotlin("jvm") version "1.4.10" 1.8 + kotlin("jvm") version "1.5.21" 1.9 war 1.10 } 1.11 group = "de.uapcore"
2.1 --- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Aug 03 12:22:10 2021 +0200 2.2 +++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt Tue Aug 03 13:41:32 2021 +0200 2.3 @@ -37,6 +37,10 @@ 2.4 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit 2.5 typealias PathParameters = Map<String, String> 2.6 2.7 +sealed interface ValidationResult<T> 2.8 +class ValidationError<T>(val message: String): ValidationResult<T> 2.9 +class ValidatedValue<T>(val result: T): ValidationResult<T> 2.10 + 2.11 class HttpRequest( 2.12 val request: HttpServletRequest, 2.13 val response: HttpServletResponse, 2.14 @@ -155,6 +159,18 @@ 2.15 fun param(name: String): String? = request.getParameter(name) 2.16 fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray() 2.17 2.18 + fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>), errorMessages: MutableList<String>): T? { 2.19 + return when (val result = validator(param(name))) { 2.20 + is ValidationError -> { 2.21 + errorMessages.add(i18n(result.message)) 2.22 + null 2.23 + } 2.24 + is ValidatedValue -> { 2.25 + result.result 2.26 + } 2.27 + } 2.28 + } 2.29 + 2.30 private fun forward(jsp: String) { 2.31 request.getRequestDispatcher(jspPath(jsp)).forward(request, response) 2.32 }
3.1 --- a/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt Tue Aug 03 12:22:10 2021 +0200 3.2 +++ b/src/main/kotlin/de/uapcore/lightpit/servlet/UsersServlet.kt Tue Aug 03 13:41:32 2021 +0200 3.3 @@ -25,12 +25,9 @@ 3.4 3.5 package de.uapcore.lightpit.servlet 3.6 3.7 -import de.uapcore.lightpit.AbstractServlet 3.8 -import de.uapcore.lightpit.HttpRequest 3.9 -import de.uapcore.lightpit.LoggingTrait 3.10 +import de.uapcore.lightpit.* 3.11 import de.uapcore.lightpit.dao.DataAccessObject 3.12 import de.uapcore.lightpit.entities.User 3.13 -import de.uapcore.lightpit.logger 3.14 import de.uapcore.lightpit.viewmodel.UserEditView 3.15 import de.uapcore.lightpit.viewmodel.UsersView 3.16 import javax.servlet.annotation.WebServlet 3.17 @@ -48,21 +45,21 @@ 3.18 private val list = "users" 3.19 private val form = "user-form" 3.20 3.21 - fun index(http: HttpRequest, dao: DataAccessObject) { 3.22 + private fun index(http: HttpRequest, dao: DataAccessObject) { 3.23 with(http) { 3.24 view = UsersView(dao.listUsers()) 3.25 render(list) 3.26 } 3.27 } 3.28 3.29 - fun create(http: HttpRequest, dao: DataAccessObject) { 3.30 + private fun create(http: HttpRequest, dao: DataAccessObject) { 3.31 with(http) { 3.32 view = UserEditView(User(-1)) 3.33 render(form) 3.34 } 3.35 } 3.36 3.37 - fun edit(http: HttpRequest, dao: DataAccessObject) { 3.38 + private fun edit(http: HttpRequest, dao: DataAccessObject) { 3.39 val id = http.pathParams["userid"]?.toIntOrNull() 3.40 if (id == null) { 3.41 http.response.sendError(404) 3.42 @@ -79,7 +76,7 @@ 3.43 } 3.44 } 3.45 3.46 - fun commit(http: HttpRequest, dao: DataAccessObject) { 3.47 + private fun commit(http: HttpRequest, dao: DataAccessObject) { 3.48 val id = http.param("userid")?.toIntOrNull() 3.49 if (id == null) { 3.50 http.response.sendError(400) 3.51 @@ -88,25 +85,32 @@ 3.52 3.53 val user = User(id) 3.54 with(user) { 3.55 - username = http.param("username") ?: "" 3.56 givenname = http.param("givenname") 3.57 lastname = http.param("lastname") 3.58 mail = http.param("mail") 3.59 } 3.60 3.61 - if (dao.findUserByName(user.username) != null) { 3.62 - with(http) { 3.63 - view = UserEditView(user).apply { errorText = "validation.username.unique" } 3.64 + if (user.id > 0) { 3.65 + logger().info("Update user with id ${user.id}.") 3.66 + dao.updateUser(user) 3.67 + http.renderCommit("users/") 3.68 + } else { 3.69 + val errorMessages = mutableListOf<String>() 3.70 + val username = http.param("username", { 3.71 + if (it == null) ValidationError("validation.username.null") 3.72 + else if (dao.findUserByName(it) != null) ValidationError("validation.username.unique") 3.73 + else ValidatedValue(it) 3.74 + }, errorMessages) 3.75 + 3.76 + if (username != null) { 3.77 + logger().info("Insert user ${username}.") 3.78 + user.username = username 3.79 + dao.insertUser(user) 3.80 + http.renderCommit("users/") 3.81 + } else { 3.82 + http.view = UserEditView(user).apply { this.errorMessages = errorMessages } 3.83 + http.render(form) 3.84 } 3.85 } 3.86 - 3.87 - if (user.id > 0) { 3.88 - logger().info("Update user ${user.username} with id ${user.id}.") 3.89 - dao.updateUser(user) 3.90 - } else { 3.91 - logger().info("Insert user ${user.username}.") 3.92 - dao.insertUser(user) 3.93 - } 3.94 - http.renderCommit("users/") 3.95 } 3.96 } 3.97 \ No newline at end of file
4.1 --- a/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Tue Aug 03 12:22:10 2021 +0200 4.2 +++ b/src/main/kotlin/de/uapcore/lightpit/types/WebColor.kt Tue Aug 03 13:41:32 2021 +0200 4.3 @@ -35,7 +35,7 @@ 4.4 /** 4.5 * The color representation with the leading hash symbol. 4.6 */ 4.7 - val color: String = (if (arg.startsWith("#")) arg else "#$arg").toUpperCase() 4.8 + val color: String = (if (arg.startsWith("#")) arg else "#$arg").uppercase() 4.9 4.10 /** 4.11 * The hex representation without the leading hash symbol.
5.1 --- a/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt Tue Aug 03 12:22:10 2021 +0200 5.2 +++ b/src/main/kotlin/de/uapcore/lightpit/viewmodel/View.kt Tue Aug 03 13:41:32 2021 +0200 5.3 @@ -27,5 +27,5 @@ 5.4 5.5 abstract class View 5.6 abstract class EditView : View() { 5.7 - var errorText: String? = null 5.8 + var errorMessages: List<String> = emptyList() 5.9 }
6.1 --- a/src/main/resources/localization/strings.properties Tue Aug 03 12:22:10 2021 +0200 6.2 +++ b/src/main/resources/localization/strings.properties Tue Aug 03 13:41:32 2021 +0200 6.3 @@ -128,6 +128,7 @@ 6.4 user.lastname=Last Name 6.5 user.mail=E-Mail 6.6 username=User Name 6.7 +validation.username.null=Username is mandatory. 6.8 validation.username.unique=Username is already taken. 6.9 version.latest=Latest Version 6.10 version.next=Next Version
7.1 --- a/src/main/resources/localization/strings_de.properties Tue Aug 03 12:22:10 2021 +0200 7.2 +++ b/src/main/resources/localization/strings_de.properties Tue Aug 03 13:41:32 2021 +0200 7.3 @@ -127,6 +127,7 @@ 7.4 user.lastname=Nachname 7.5 user.mail=E-Mail 7.6 username=Benutzername 7.7 +validation.username.null=Benutzername ist ein Pflichtfeld. 7.8 validation.username.unique=Der Benutzername wird bereits verwendet. 7.9 version.latest=Neuste Version 7.10 version.next=N\u00e4chste Version
8.1 --- a/src/main/webapp/WEB-INF/jsp/user-form.jsp Tue Aug 03 12:22:10 2021 +0200 8.2 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp Tue Aug 03 13:41:32 2021 +0200 8.3 @@ -38,34 +38,30 @@ 8.4 <col style="width: 50ch"> 8.5 </colgroup> 8.6 <tbody> 8.7 + <c:if test="${not empty viewmodel.errorMessages}"> 8.8 + <tr> 8.9 + <td colspan="2"><%@include file="../jspf/error-messages.jspf" %></td> 8.10 + </tr> 8.11 + </c:if> 8.12 <tr> 8.13 <th><fmt:message key="username"/></th> 8.14 - <td><input name="username" type="text" maxlength="50" required value="<c:out value="${user.username}"/>" 8.15 + <td><input name="username" type="text" maxlength="200" required value="<c:out value="${user.username}"/>" 8.16 <c:if test="${user.id ge 0}">readonly</c:if> /></td> 8.17 </tr> 8.18 <tr> 8.19 <th><fmt:message key="user.givenname"/></th> 8.20 - <td><input name="givenname" type="text" maxlength="50" value="<c:out value="${user.givenname}"/>" /></td> 8.21 + <td><input name="givenname" type="text" maxlength="200" value="<c:out value="${user.givenname}"/>" /></td> 8.22 </tr> 8.23 <tr> 8.24 <th><fmt:message key="user.lastname"/></th> 8.25 - <td><input name="lastname" type="text" maxlength="50" value="<c:out value="${user.lastname}"/>" /></td> 8.26 + <td><input name="lastname" type="text" maxlength="200" value="<c:out value="${user.lastname}"/>" /></td> 8.27 </tr> 8.28 <tr> 8.29 <th><fmt:message key="user.mail"/></th> 8.30 - <td><input name="mail" type="email" maxlength="50" value="<c:out value="${user.mail}"/>" /></td> 8.31 + <td><input name="mail" type="email" maxlength="200" value="<c:out value="${user.mail}"/>" /></td> 8.32 </tr> 8.33 </tbody> 8.34 <tfoot> 8.35 - <c:if test="${not empty viewmodel.errorText}"> 8.36 - <tr> 8.37 - <td colspan="2"> 8.38 - <div class="error-box"> 8.39 - <fmt:message key="${viewmodel.errorText}"/> 8.40 - </div> 8.41 - </td> 8.42 - </tr> 8.43 - </c:if> 8.44 <tr> 8.45 <td colspan="2"> 8.46 <input type="hidden" name="userid" value="${user.id}"/>
9.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 9.2 +++ b/src/main/webapp/WEB-INF/jspf/error-messages.jspf Tue Aug 03 13:41:32 2021 +0200 9.3 @@ -0,0 +1,34 @@ 9.4 +<%-- 9.5 + ~ Copyright 2021 Mike Becker. All rights reserved. 9.6 + ~ 9.7 + ~ Redistribution and use in source and binary forms, with or without 9.8 + ~ modification, are permitted provided that the following conditions are met: 9.9 + ~ 9.10 + ~ 1. Redistributions of source code must retain the above copyright 9.11 + ~ notice, this list of conditions and the following disclaimer. 9.12 + ~ 9.13 + ~ 2. Redistributions in binary form must reproduce the above copyright 9.14 + ~ notice, this list of conditions and the following disclaimer in the 9.15 + ~ documentation and/or other materials provided with the distribution. 9.16 + ~ 9.17 + ~ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 9.18 + ~ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 9.19 + ~ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 9.20 + ~ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 9.21 + ~ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 9.22 + ~ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 9.23 + ~ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 9.24 + ~ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 9.25 + ~ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 9.26 + ~ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 9.27 + --%> 9.28 +<%@page pageEncoding="UTF-8" %> 9.29 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 9.30 + 9.31 +<div class="error-box hcenter"> 9.32 + <c:forEach var="errorMessage" items="${viewmodel.errorMessages}"> 9.33 + <div> 9.34 + <c:out value="${errorMessage}"/> 9.35 + </div> 9.36 + </c:forEach> 9.37 +</div>
10.1 --- a/src/main/webapp/lightpit.css Tue Aug 03 12:22:10 2021 +0200 10.2 +++ b/src/main/webapp/lightpit.css Tue Aug 03 13:41:32 2021 +0200 10.3 @@ -198,6 +198,7 @@ 10.4 10.5 table.formtable tbody td > * { 10.6 width: 100%; 10.7 + margin: 0; 10.8 box-sizing: border-box; 10.9 } 10.10 10.11 @@ -238,7 +239,7 @@ 10.12 } 10.13 10.14 .info-box, .error-box, .warn-box { 10.15 - margin: 2em; 10.16 + margin: 1.5em; 10.17 border-style: dashed; 10.18 border-width: thin; 10.19 border-color: deepskyblue; 10.20 @@ -246,11 +247,15 @@ 10.21 } 10.22 10.23 .error-box { 10.24 + border-style: outset; 10.25 border-color: red; 10.26 + background: lightcoral; 10.27 } 10.28 10.29 .warn-box { 10.30 + border-style: outset; 10.31 border-color: gold; 10.32 + background: lightgoldenrodyellow; 10.33 } 10.34 10.35 .table {