Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 8.1 snapshot #263

Merged
merged 7 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public void onStartup(ServletContext sc) throws ServletException {
securityHeaderFilter.addMappingForUrlPatterns(null, false, "/*");
securityHeaderFilter.setInitParameter(SecurityHeaderFilter.PARAM_CSP_HEADER_VALUE, cspHeaderValue);

if (pfSpringConfiguration.getCorsFilterEnabled()) {
log.warn("************* Enabling CorsPreflightFilter for development. *************");
FilterRegistration.Dynamic corsPreflightFilter = sc.addFilter("CorsPreflightFilter", CorsPreflightFilter.class);
corsPreflightFilter.addMappingForUrlPatterns(null, false, "/*");
}
/*
* Redirect orphaned links from former versions of ProjectForge (e. g. if link in e-mails were changed due to migrations or refactoring.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/////////////////////////////////////////////////////////////////////////////
//
// Project ProjectForge Community Edition
// www.projectforge.org
//
// Copyright (C) 2001-2025 Micromata GmbH, Germany (www.micromata.com)
//
// ProjectForge is dual-licensed.
//
// This community edition is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published
// by the Free Software Foundation; version 3 of the License.
//
// This community edition is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see http://www.gnu.org/licenses/.
//
/////////////////////////////////////////////////////////////////////////////

package org.projectforge.config

import jakarta.servlet.*
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.projectforge.framework.configuration.PFSpringConfiguration
import java.io.IOException


class CorsPreflightFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request as HttpServletRequest
val httpResponse = response as HttpServletResponse
val allowedOrigins = PFSpringConfiguration.getInstance().corsAllowedOrigins
httpResponse.setHeader("Access-Control-Allow-Origin", allowedOrigins)
httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
httpResponse.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With")
httpResponse.setHeader("Access-Control-Allow-Credentials", "true") // Erlaubt Credentials
httpResponse.setHeader("Access-Control-Max-Age", "3600")

// Preflight-Request (OPTIONS) direkt beantworten
if ("OPTIONS".equals(httpRequest.method, ignoreCase = true)) {
httpResponse.status = HttpServletResponse.SC_OK
} else {
// Andere Anfragen weiterleiten
chain.doFilter(request, response)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import java.util.*
),
NamedQuery(
name = TimesheetDO.SELECT_RECENT_USED_LOCATIONS_BY_USER_AND_LAST_UPDATE,
query = "select distinct location from TimesheetDO where deleted=false and user.id=:userId and lastUpdate>:lastUpdate and location!=null and location!='' order by location"
query = "select distinct location from TimesheetDO where deleted=false and user.id=:userId and lastUpdate>:lastUpdate and location!='' order by location"
),
NamedQuery(
name = TimesheetDO.SELECT_REFERENCES_BY_TASK_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ open class TimesheetDao : BaseDao<TimesheetDO>(TimesheetDO::class.java) {
/**
* Get all locations of the user's time sheet (not deleted ones) with modification date within last year.
*/
@Deprecated("Used by deprecated Wicket pages.")
open fun getLocationAutocompletion(searchString: String?): List<String> {
checkLoggedInUserSelectAccess()
val oneYearAgo = now().minusDays(365)
Expand Down Expand Up @@ -712,11 +713,11 @@ open class TimesheetDao : BaseDao<TimesheetDO>(TimesheetDO::class.java) {
*/
open fun getRecentLocation(sinceDate: Date?): Collection<String> {
checkLoggedInUserSelectAccess()
log.info("Get recent locations from the database.")
log.debug { "Get recent locations from the database." }
return persistenceService.executeNamedQuery(
TimesheetDO.SELECT_RECENT_USED_LOCATIONS_BY_USER_AND_LAST_UPDATE,
String::class.java,
Pair("userId", ThreadLocalUserContext.loggedInUserId),
Pair("userId", ThreadLocalUserContext.requiredLoggedInUserId),
Pair("lastUpdate", sinceDate),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ private val log = KotlinLogging.logger {}
* @author Kai Reinhard ([email protected])
*/
object ExcelUtils {
/**
* Sets the active sheet and deselects all other sheets.
*/
@JvmStatic
fun setActiveSheet(workbook: ExcelWorkbook, activeSheetIndex: Int) {
for (i in 0..<workbook.numberOfSheets) {
workbook.getSheet(i).poiSheet.setSelected(i == activeSheetIndex)
}
workbook.setActiveSheet(activeSheetIndex)
}

/**
* Should be used for workbook creation for every export.
* @return workbook configured with date formats of the logged-in user and number formats.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class Html {
}
}

class Alert(type: Type, content: String? = null, id: String? = null) :
HtmlElement("div", content = content, id = id) {
class Alert(type: Type, content: String? = null, id: String? = null, replaceNewlinesByBr: Boolean = true) :
HtmlElement("div", content = content, id = id, replaceNewlinesByBr = replaceNewlinesByBr) {
enum class Type(val cls: String) {
INFO("alert-info"),
SUCCESS("alert-success"),
Expand All @@ -45,21 +45,22 @@ class Html {
}
}

class P(content: String? = null, style: String? = null, id: String? = null) :
HtmlElement("p", content = content, id = id) {
class P(content: String? = null, style: String? = null, id: String? = null, replaceNewlinesByBr: Boolean = true) :
HtmlElement("p", content = content, id = id, replaceNewlinesByBr = replaceNewlinesByBr) {
init {
style?.let { attr("style", it) }
}
}

class Span(content: String, style: String? = null, id: String? = null) :
HtmlElement("span", content = content, id = id) {
class Span(content: String, style: String? = null, id: String? = null, replaceNewlinesByBr: Boolean = true) :
HtmlElement("span", content = content, id = id, replaceNewlinesByBr = replaceNewlinesByBr) {
init {
style?.let { attr("style", it) }
}
}

class Text(content: String) : HtmlElement("NOTAG", content = content, childrenAllowed = false)
class Text(content: String, replaceNewlinesByBr: Boolean = true) :
HtmlElement("NOTAG", content = content, childrenAllowed = false, replaceNewlinesByBr = replaceNewlinesByBr)

class BR : HtmlElement("br", childrenAllowed = false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,35 @@

package org.projectforge.common.html

/**
* Represents an HTML element.
* @param tag The tag of the element
* @param content The content of the element ('\n' will be replaced by '<br />').
* @param childrenAllowed Whether children are allowed for this element
* @param id The id of the element
* @param replaceNewlinesByBr Whether newlines in the content should be replaced by '<br />'
*/
open class HtmlElement(
val tag: String,
val content: String? = null,
content: String? = null,
val childrenAllowed: Boolean = true,
val id: String? = null
val id: String? = null,
replaceNewlinesByBr: Boolean = false,
) {
val content: String? = if (content != null && childrenAllowed && replaceNewlinesByBr && content.contains('\n')) {
// Example: "Hello\nWorld" -> "Hello\n<br />\nWorld"
content.split('\n').forEachIndexed { index, s ->
if (index > 0) {
this.add(Html.BR())
}
if (s.isNotBlank()) {
add(Html.Text("$s\n", replaceNewlinesByBr = false))
}
}
null
} else {
content
}
var children: MutableList<HtmlElement>? = null
var attributes: MutableMap<String, String>? = null
var classnames: MutableList<String>? = null
Expand Down Expand Up @@ -61,10 +84,21 @@ open class HtmlElement(
return this
}

/**
* Adds a text element to this element. Convenience method for adding text to an element.
*/
fun addText(text: String): HtmlElement {
return add(Html.Text(text))
}

open fun append(sb: StringBuilder, indent: Int) {
if (this is Html.Text) {
// Special case for text elements: Just append the content
sb.append(escape(content))
if (!content.isNullOrBlank()) {
indent(sb, indent)
sb.append(escape(content.trim()))
sb.append("\n")
}
return
}
if (children.isNullOrEmpty() && content.isNullOrEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ class JobListExecutionContext {
when (status) {
Status.OK -> html.add(Html.Alert(Html.Alert.Type.SUCCESS, "All checks passed successfully. $time"))
Status.WARNINGS -> html.add(Html.Alert(Html.Alert.Type.WARNING, "Some checks passed with warnings. $time"))
Status.ERRORS -> html.add(Html.Alert(Html.Alert.Type.DANGER, "Some errors occurred! $time"))
Status.ERRORS -> html.add(
Html.Alert(
Html.Alert.Type.DANGER,
"Some errors occurred! $time\n\nIt's recommended to re-run the checks for double-check."
)
)
}
html.add(HtmlTable().also { table ->
table.addHeadRow().also { tr ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
package org.projectforge.rest.core

import org.projectforge.common.logging.LogLevel
import org.projectforge.framework.i18n.InternalErrorException
import org.springframework.http.HttpStatus
import org.springframework.web.context.request.async.AsyncRequestNotUsableException
import org.springframework.web.context.request.async.AsyncRequestTimeoutException
Expand All @@ -32,6 +33,11 @@ import java.io.IOException
import java.net.ConnectException
import java.net.NoRouteToHostException

/**
* Registry for known exceptions. For known exceptions, the log level, status and message can be defined. You may
* also define a match function to match the exception as well as if an email should be sent to the developers.
* This class is thread-safe.
*/
object GlobalExceptionRegistry {
open class ExInfo(
val message: String? = null,
Expand All @@ -41,13 +47,15 @@ object GlobalExceptionRegistry {
val knownExceptionName: String? = null,
val knownExceptionMessagePart: String? = null,
val sendMailToDevelopers: Boolean = false,
val match: ((ex: Throwable) -> Boolean)? = null
) {
/**
* @param ex The exception to check.
* @return True if the exception matches this ExInfo (all given values: exception, exceptionName and messagePart, if given).
*/
open fun match(ex: Throwable): Boolean {
return matchKnownException(ex) && matchKnownExceptionName(ex) && matchKnownExceptionMessagePart(ex)
return match?.invoke(ex)
?: (matchKnownException(ex) && matchKnownExceptionName(ex) && matchKnownExceptionMessagePart(ex))
}

private fun matchKnownException(ex: Throwable): Boolean {
Expand All @@ -59,7 +67,10 @@ object GlobalExceptionRegistry {
}

private fun matchKnownExceptionMessagePart(ex: Throwable): Boolean {
return knownExceptionMessagePart == null || ex.message?.contains(knownExceptionMessagePart) == true
return knownExceptionMessagePart == null || ex.message?.contains(
knownExceptionMessagePart,
ignoreCase = true
) == true
}
}

Expand All @@ -73,6 +84,7 @@ object GlobalExceptionRegistry {
logLevel: LogLevel = LogLevel.ERROR,
status: HttpStatus = HttpStatus.BAD_REQUEST,
knownExceptionMessagePart: String? = null,
match: ((ex: Throwable) -> Boolean)? = null,
) {
registerExInfo(
ExInfo(
Expand All @@ -81,6 +93,7 @@ object GlobalExceptionRegistry {
status = status,
knownException = knownException,
knownExceptionMessagePart = knownExceptionMessagePart,
match = match,
)
)
}
Expand All @@ -98,16 +111,17 @@ object GlobalExceptionRegistry {
registerExInfo(AsyncRequestTimeoutException::class.java, "Asyn client connection lost (OK)", LogLevel.INFO)
registerExInfo(java.lang.IllegalStateException::class.java, knownExceptionMessagePart = "Cannot start async")
registerExInfo(NoResourceFoundException::class.java, "Resource not found", status = HttpStatus.NOT_FOUND)
registerExInfo(object : ExInfo(message = "Connection lost") {
override fun match(ex: Throwable): Boolean {
if (ex !is IOException) {
return false
}
val message = ex.message?.lowercase()
return message?.contains("broken pipe") == true || message?.contains("connection reset by peer") == true
// Die Verbindung wurde vom Kommunikationspartner zurückgesetzt
|| message?.contains("verbindung wurde vom kommunikationspartner") == true
}
registerExInfo(
InternalErrorException::class.java,
"Csrf error",
LogLevel.WARN,
knownExceptionMessagePart = "csrf"
)
registerExInfo(IOException::class.java, "Connection lost", LogLevel.INFO, match = { ex ->
val message = ex.message?.lowercase()
message?.contains("broken pipe") == true || message?.contains("connection reset by peer") == true
// Die Verbindung wurde vom Kommunikationspartner zurückgesetzt
|| message?.contains("verbindung wurde vom kommunikationspartner") == true
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ class RestUserFilter : AbstractRestUserFilter(UserTokenType.REST_CLIENT) {
val requestURI = authInfo.request.requestURI
// Don't log error for userStatus (used by React client for checking weather the user is logged in or not).
if (requestURI == null || requestURI != "/rs/userStatus") {
val method = authInfo.request.method
val msg =
"Neither valid session-credentials (request.sessionId=${RequestLog.getTruncatedSessionId(authInfo.request)}), ${Authentication.AUTHENTICATION_USER_ID} nor ${Authentication.AUTHENTICATION_USERNAME}/${Authentication.AUTHENTICATION_TOKEN} is given for rest call: $requestURI. Rest call forbidden."
"Neither valid session-credentials (request.sessionId=${RequestLog.getTruncatedSessionId(authInfo.request)}), ${Authentication.AUTHENTICATION_USER_ID} nor ${Authentication.AUTHENTICATION_USERNAME}/${Authentication.AUTHENTICATION_TOKEN} is given for rest call: $method:$requestURI. Rest call forbidden."
log.error(msg)
SecurityLogging.logSecurityWarn(authInfo.request, this::class.java, "REST AUTHENTICATION FAILED", msg)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ function ReactSelect(

return (
<div className="react-select">
{tooltipElement}
<Tag
cache={{}}
className={classNames(
Expand Down Expand Up @@ -124,6 +123,8 @@ function ReactSelect(
value={value || null}
{...props}
/>
{tooltipElement}

{additionalLabel && (
<span className="react-select__additional-label">{additionalLabel}</span>
)}
Expand Down
Loading