Update to fiber 3

Change-Id: I18a7b3ed55aef5cae67db6a613ec14e0f8a9281e
diff --git a/cmd/koralmapper/fuzz_test.go b/cmd/koralmapper/fuzz_test.go
index 39f20c3..92c21e3 100644
--- a/cmd/koralmapper/fuzz_test.go
+++ b/cmd/koralmapper/fuzz_test.go
@@ -13,7 +13,7 @@
 
 	tmconfig "github.com/KorAP/Koral-Mapper/config"
 	"github.com/KorAP/Koral-Mapper/mapper"
-	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/fiber/v3"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -56,8 +56,7 @@
 
 	// Create fiber app
 	app := fiber.New(fiber.Config{
-		DisableStartupMessage: true,
-		ErrorHandler: func(c *fiber.Ctx, err error) error {
+		ErrorHandler: func(c fiber.Ctx, err error) error {
 			// For body limit errors, return 413 status code
 			if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
 				return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
@@ -176,8 +175,7 @@
 
 	// Create fiber app
 	app := fiber.New(fiber.Config{
-		DisableStartupMessage: true,
-		ErrorHandler: func(c *fiber.Ctx, err error) error {
+		ErrorHandler: func(c fiber.Ctx, err error) error {
 			// For body limit errors, return 413 status code
 			if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
 				return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
@@ -289,8 +287,7 @@
 
 	// Create fiber app
 	app := fiber.New(fiber.Config{
-		DisableStartupMessage: true,
-		ErrorHandler: func(c *fiber.Ctx, err error) error {
+		ErrorHandler: func(c fiber.Ctx, err error) error {
 			// For body limit errors, return 413 status code
 			if err.Error() == "body size exceeds the given limit" || errors.Is(err, fiber.ErrRequestEntityTooLarge) {
 				return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
diff --git a/cmd/koralmapper/main.go b/cmd/koralmapper/main.go
index 66cd96b..ccea13b 100644
--- a/cmd/koralmapper/main.go
+++ b/cmd/koralmapper/main.go
@@ -19,9 +19,9 @@
 	"github.com/KorAP/Koral-Mapper/config"
 	"github.com/KorAP/Koral-Mapper/mapper"
 	"github.com/alecthomas/kong"
-	"github.com/gofiber/fiber/v2"
-	"github.com/gofiber/fiber/v2/middleware/cors"
-	"github.com/gofiber/fiber/v2/middleware/limiter"
+	"github.com/gofiber/fiber/v3"
+	"github.com/gofiber/fiber/v3/middleware/cors"
+	"github.com/gofiber/fiber/v3/middleware/limiter"
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/log"
 )
@@ -140,12 +140,12 @@
 
 	// Only enable HTTP request logging if log level is debug or info
 	if currentLevel > zerolog.InfoLevel {
-		return func(c *fiber.Ctx) error {
+		return func(c fiber.Ctx) error {
 			return c.Next()
 		}
 	}
 
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		// Record start time
 		start := time.Now()
 
@@ -179,9 +179,14 @@
 }
 
 // extractRequestParams extracts and validates common request parameters
-func extractRequestParams(c *fiber.Ctx) (*requestParams, error) {
+func extractRequestParams(c fiber.Ctx) (*requestParams, error) {
+	mapID, err := url.PathUnescape(c.Params("map"))
+	if err != nil {
+		return nil, fmt.Errorf("mapID contains invalid characters")
+	}
+
 	params := &requestParams{
-		MapID:    c.Params("map"),
+		MapID:    mapID,
 		Dir:      c.Query("dir", "atob"),
 		FoundryA: c.Query("foundryA", ""),
 		FoundryB: c.Query("foundryB", ""),
@@ -208,9 +213,9 @@
 }
 
 // parseRequestBody parses JSON request body and direction
-func parseRequestBody(c *fiber.Ctx, dir string) (any, mapper.Direction, error) {
+func parseRequestBody(c fiber.Ctx, dir string) (any, mapper.Direction, error) {
 	var jsonData any
-	if err := c.BodyParser(&jsonData); err != nil {
+	if err := c.Bind().Body(&jsonData); err != nil {
 		return nil, mapper.BtoA, fmt.Errorf("invalid JSON in request body")
 	}
 
@@ -280,10 +285,9 @@
 
 	// Create fiber app
 	app := fiber.New(fiber.Config{
-		DisableStartupMessage: true,
-		BodyLimit:             maxInputLength,
-		ReadBufferSize:        64 * 1024, // 64KB - increase header size limit
-		WriteBufferSize:       64 * 1024, // 64KB - increase response buffer size
+		BodyLimit:       maxInputLength,
+		ReadBufferSize:  64 * 1024, // 64KB - increase header size limit
+		WriteBufferSize: 64 * 1024, // 64KB - increase response buffer size,
 	})
 
 	// Add zerolog-integrated logger middleware
@@ -305,7 +309,7 @@
 			)
 		}
 
-		if err := app.Listen(fmt.Sprintf(":%d", finalPort)); err != nil {
+		if err := app.Listen(fmt.Sprintf(":%d", finalPort), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
 			log.Fatal().Err(err).Msg("Server error")
 		}
 	}()
@@ -330,7 +334,7 @@
 	// information leaks (OWASP Secure Headers). X-Frame-Options is
 	// intentionally omitted because the service is designed to be embedded
 	// in cross-origin iframes (Kalamar plugin).
-	app.Use(func(c *fiber.Ctx) error {
+	app.Use(func(c fiber.Ctx) error {
 		c.Set("X-Content-Type-Options", "nosniff")
 		c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
 		return c.Next()
@@ -344,8 +348,8 @@
 	// (default: "https://korap.ids-mannheim.de").
 	app.Use(cors.New(cors.Config{
 		AllowOrigins: yamlConfig.AllowOrigins,
-		AllowMethods: "GET,POST",
-		AllowHeaders: "Content-Type",
+		AllowMethods: []string{"GET", "POST"},
+		AllowHeaders: []string{"Content-Type"},
 	}))
 
 	// Rate limiting middleware to prevent resource exhaustion from
@@ -363,7 +367,7 @@
 	}))
 
 	// Health check endpoint
-	app.Get("/health", func(c *fiber.Ctx) error {
+	app.Get("/health", func(c fiber.Ctx) error {
 		return c.SendString("OK")
 	})
 
@@ -386,7 +390,7 @@
 }
 
 func handleStaticFile() fiber.Handler {
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		name := c.Params("*")
 		data, err := fs.ReadFile(staticFS, "static/"+name)
 		if err != nil {
@@ -468,7 +472,7 @@
 		listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
 	}
 
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		cfgRaw := c.Params("cfg")
 		if len(cfgRaw) > maxParamLength {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -477,7 +481,7 @@
 		}
 
 		var jsonData any
-		if err := c.BodyParser(&jsonData); err != nil {
+		if err := c.Bind().Body(&jsonData); err != nil {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 				"error": "invalid JSON in request body",
 			})
@@ -548,7 +552,7 @@
 		listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
 	}
 
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		cfgRaw := c.Params("cfg")
 		if len(cfgRaw) > maxParamLength {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -557,7 +561,7 @@
 		}
 
 		var jsonData any
-		if err := c.BodyParser(&jsonData); err != nil {
+		if err := c.Bind().Body(&jsonData); err != nil {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 				"error": "invalid JSON in request body",
 			})
@@ -628,7 +632,7 @@
 		listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
 	}
 
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		// Extract and validate parameters
 		params, err := extractRequestParams(c)
 		if err != nil {
@@ -685,7 +689,7 @@
 		listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
 	}
 
-	return func(c *fiber.Ctx) error {
+	return func(c fiber.Ctx) error {
 		// Extract and validate parameters
 		params, err := extractRequestParams(c)
 		if err != nil {
@@ -769,8 +773,8 @@
 }
 
 func handleKalamarPlugin(yamlConfig *config.MappingConfig, configTmpl *template.Template, pluginTmpl *template.Template) fiber.Handler {
-	return func(c *fiber.Ctx) error {
-		mapID := c.Params("map")
+	return func(c fiber.Ctx) error {
+		mapID, _ := url.PathUnescape(c.Params("map"))
 
 		// Config page (GET /)
 		if mapID == "" {
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index d0d77f2..475d0dd 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -16,7 +16,7 @@
 
 	tmconfig "github.com/KorAP/Koral-Mapper/config"
 	"github.com/KorAP/Koral-Mapper/mapper"
-	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/fiber/v3"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -2797,7 +2797,7 @@
 	require.NoError(t, err)
 
 	mockConfig := &tmconfig.MappingConfig{
-		AllowOrigins: "https://custom.example.com",
+		AllowOrigins: []string{"https://custom.example.com"},
 		Lists:        []tmconfig.MappingList{mappingList},
 	}
 	tmconfig.ApplyDefaults(mockConfig)
@@ -2830,7 +2830,7 @@
 	require.NoError(t, err)
 
 	mockConfig := &tmconfig.MappingConfig{
-		AllowOrigins: "https://allowed.example.com",
+		AllowOrigins: []string{"https://allowed.example.com"},
 		Lists:        []tmconfig.MappingList{mappingList},
 	}
 	tmconfig.ApplyDefaults(mockConfig)
@@ -2860,7 +2860,7 @@
 	require.NoError(t, err)
 
 	mockConfig := &tmconfig.MappingConfig{
-		AllowOrigins: "https://first.example.com,https://second.example.com",
+		AllowOrigins: []string{"https://first.example.com", "https://second.example.com"},
 		Lists:        []tmconfig.MappingList{mappingList},
 	}
 	tmconfig.ApplyDefaults(mockConfig)