src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt

Tue, 03 Aug 2021 13:41:32 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 03 Aug 2021 13:41:32 +0200
changeset 209
c9c6abf167c7
parent 207
479dd7993ef9
child 210
37fbdcb422b7
permissions
-rw-r--r--

#21 adds input validation mechanism

Also upgrades to Kotlin 1.5.21

     1 /*
     2  * Copyright 2021 Mike Becker. All rights reserved.
     3  *
     4  * Redistribution and use in source and binary forms, with or without
     5  * modification, are permitted provided that the following conditions are met:
     6  *
     7  * 1. Redistributions of source code must retain the above copyright
     8  * notice, this list of conditions and the following disclaimer.
     9  *
    10  * 2. Redistributions in binary form must reproduce the above copyright
    11  * notice, this list of conditions and the following disclaimer in the
    12  * documentation and/or other materials provided with the distribution.
    13  *
    14  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    17  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    20  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    21  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    22  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    24  */
    26 package de.uapcore.lightpit
    28 import de.uapcore.lightpit.dao.DataAccessObject
    29 import de.uapcore.lightpit.viewmodel.NavMenu
    30 import de.uapcore.lightpit.viewmodel.View
    31 import java.util.*
    32 import javax.servlet.http.HttpServletRequest
    33 import javax.servlet.http.HttpServletResponse
    34 import javax.servlet.http.HttpSession
    35 import kotlin.math.min
    37 typealias MappingMethod = (HttpRequest, DataAccessObject) -> Unit
    38 typealias PathParameters = Map<String, String>
    40 sealed interface ValidationResult<T>
    41 class ValidationError<T>(val message: String): ValidationResult<T>
    42 class ValidatedValue<T>(val result: T): ValidationResult<T>
    44 class HttpRequest(
    45     val request: HttpServletRequest,
    46     val response: HttpServletResponse,
    47     val pathParams: PathParameters = emptyMap()
    48 ) {
    49     val session: HttpSession = request.session
    51     val remoteUser: String? = request.remoteUser
    53     /**
    54      * The name of the content page.
    55      *
    56      * @see Constants#REQ_ATTR_CONTENT_PAGE
    57      */
    58     var contentPage = ""
    59         set(value) {
    60             field = value
    61             request.setAttribute(Constants.REQ_ATTR_CONTENT_PAGE, jspPath(value))
    62         }
    64     /**
    65      * The name of the content page.
    66      *
    67      * @see Constants#REQ_ATTR_PAGE_TITLE
    68      */
    69     var pageTitle = ""
    70         set(value) {
    71             field = value
    72             request.setAttribute(Constants.REQ_ATTR_PAGE_TITLE, value)
    73         }
    75     /**
    76      * A list of additional style sheets.
    77      *
    78      * @see Constants#REQ_ATTR_STYLESHEET
    79      */
    80     var styleSheets = emptyList<String>()
    81         set(value) {
    82             field = value
    83             request.setAttribute(Constants.REQ_ATTR_STYLESHEET,
    84                 value.map { it.withExt(".css") }
    85             )
    86         }
    88     /**
    89      * A list of additional style sheets.
    90      *
    91      * @see Constants#REQ_ATTR_JAVASCRIPT
    92      */
    93     var javascript = ""
    94         set(value) {
    95             field = value
    96             request.setAttribute(Constants.REQ_ATTR_JAVASCRIPT,
    97                 value.withExt(".js")
    98             )
    99         }
   101     /**
   102      * The name of the navigation menu JSP.
   103      *
   104      * @see Constants#REQ_ATTR_NAVIGATION
   105      */
   106     var navigationMenu: NavMenu? = null
   107         set(value) {
   108             field = value
   109             request.setAttribute(Constants.REQ_ATTR_NAVIGATION, navigationMenu)
   110         }
   112     var redirectLocation: String? = null
   113         set(value) {
   114             field = value
   115             if (value == null) {
   116                 request.removeAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION)
   117             } else {
   118                 request.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, baseHref + value)
   119             }
   120         }
   122     var feedPath: String? = null
   123         set(value) {
   124             field = value
   125             if (value == null) {
   126                 request.removeAttribute(Constants.REQ_ATTR_FEED_HREF)
   127             } else {
   128                 request.setAttribute(Constants.REQ_ATTR_FEED_HREF, baseHref + value)
   129             }
   130         }
   132     /**
   133      * The view object.
   134      *
   135      * @see Constants#REQ_ATTR_VIEWMODEL
   136      */
   137     var view: View? = null
   138         set(value) {
   139             field = value
   140             request.setAttribute(Constants.REQ_ATTR_VIEWMODEL, value)
   141         }
   143     /**
   144      * Additional port info, if necessary.
   145      */
   146     private val portInfo =
   147         if ((request.scheme == "http" && request.serverPort == 80)
   148             || (request.scheme == "https" && request.serverPort == 443)
   149         ) "" else ":${request.serverPort}"
   151     /**
   152      * The base path of this application.
   153      */
   154     val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
   156     private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
   157     private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
   159     fun param(name: String): String? = request.getParameter(name)
   160     fun paramArray(name: String): Array<String> = request.getParameterValues(name) ?: emptyArray()
   162     fun <T> param(name: String, validator: (String?) -> (ValidationResult<T>), errorMessages: MutableList<String>): T? {
   163         return when (val result = validator(param(name))) {
   164             is ValidationError -> {
   165                 errorMessages.add(i18n(result.message))
   166                 null
   167             }
   168             is ValidatedValue -> {
   169                 result.result
   170             }
   171         }
   172     }
   174     private fun forward(jsp: String) {
   175         request.getRequestDispatcher(jspPath(jsp)).forward(request, response)
   176     }
   178     fun renderFeed(page: String? = null) {
   179         page?.let { contentPage = it }
   180         forward("feed")
   181     }
   183     fun render(page: String? = null) {
   184         page?.let { contentPage = it }
   185         forward("site")
   186     }
   188     fun renderCommit(location: String? = null) {
   189         location?.let { redirectLocation = it }
   190         contentPage = Constants.JSP_COMMIT_SUCCESSFUL
   191         render()
   192     }
   194     fun i18n(key: String) = ResourceBundle.getBundle("localization/strings", response.locale).getString(key)
   195 }
   197 /**
   198  * A path pattern optionally containing placeholders.
   199  *
   200  * The special directories . and .. are disallowed in the pattern.
   201  * Placeholders start with a % sign.
   202  *
   203  * @param pattern the pattern
   204  */
   205 class PathPattern(pattern: String) {
   206     private val nodePatterns: List<String>
   207     private val collection: Boolean
   209     private fun parse(pattern: String): List<String> {
   210         val nodes = pattern.split("/").filter { it.isNotBlank() }.toList()
   211         require(nodes.none { it == "." || it == ".." }) { "Path must not contain '.' or '..' nodes." }
   212         return nodes
   213     }
   215     /**
   216      * Matches a path against this pattern.
   217      * The path must be canonical in the sense that no . or .. parts occur.
   218      *
   219      * @param path the path to match
   220      * @return true if the path matches the pattern, false otherwise
   221      */
   222     fun matches(path: String): Boolean {
   223         if (collection xor path.endsWith("/")) return false
   224         val nodes = parse(path)
   225         if (nodePatterns.size != nodes.size) return false
   226         for (i in nodePatterns.indices) {
   227             val pattern = nodePatterns[i]
   228             val node = nodes[i]
   229             if (pattern.startsWith("%")) continue
   230             if (pattern != node) return false
   231         }
   232         return true
   233     }
   235     /**
   236      * Returns the path parameters found in the specified path using this pattern.
   237      * The return value of this method is undefined, if the patter does not match.
   238      *
   239      * @param path the path
   240      * @return the path parameters, if any, or an empty map
   241      * @see .matches
   242      */
   243     fun obtainPathParameters(path: String): PathParameters {
   244         val params = mutableMapOf<String, String>()
   245         val nodes = parse(path)
   246         for (i in 0 until min(nodes.size, nodePatterns.size)) {
   247             val pattern = nodePatterns[i]
   248             val node = nodes[i]
   249             if (pattern.startsWith("%")) {
   250                 params[pattern.substring(1)] = node
   251             }
   252         }
   253         return params
   254     }
   256     override fun hashCode(): Int {
   257         val str = StringBuilder()
   258         for (node in nodePatterns) {
   259             if (node.startsWith("%")) {
   260                 str.append("/%")
   261             } else {
   262                 str.append('/')
   263                 str.append(node)
   264             }
   265         }
   266         if (collection) str.append('/')
   267         return str.toString().hashCode()
   268     }
   270     override fun equals(other: Any?): Boolean {
   271         if (other is PathPattern) {
   272             if (collection xor other.collection || nodePatterns.size != other.nodePatterns.size) return false
   273             for (i in nodePatterns.indices) {
   274                 val left = nodePatterns[i]
   275                 val right = other.nodePatterns[i]
   276                 if (left.startsWith("%") && right.startsWith("%")) continue
   277                 if (left != right) return false
   278             }
   279             return true
   280         } else {
   281             return false
   282         }
   283     }
   285     init {
   286         nodePatterns = parse(pattern)
   287         collection = pattern.endsWith("/")
   288     }
   289 }

mercurial