From 8b5a2494502fa6cb1a983905152c9536b003a377 Mon Sep 17 00:00:00 2001 From: Greg Sarjeant Date: Thu, 31 Jul 2025 02:39:09 +0000 Subject: [PATCH] Make URL building more resilient and add tests. (#38) Since the base URL and base path are user inputs, I'd like tkr to be resilient to any combination of leading and trailing slashes so people don't have to worry about that. This adds some helper functions to normalize URLs and adds tests to confirm that all combinations are handled correctly. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/38 Co-authored-by: Greg Sarjeant Co-committed-by: Greg Sarjeant --- public/index.php | 2 +- .../AdminController/AdminController.php | 2 +- .../AuthController/AuthController.php | 4 +- .../EmojiController/EmojiController.php | 2 +- .../HomeController/HomeController.php | 2 +- .../MoodController/MoodController.php | 2 +- src/Framework/Util/Util.php | 24 ++++++++++ templates/main.php | 4 +- templates/partials/admin.php | 2 +- templates/partials/css.php | 2 +- templates/partials/emoji.php | 4 +- templates/partials/home.php | 2 +- templates/partials/login.php | 2 +- templates/partials/navbar.php | 16 +++---- tests/Framework/Util/UtilTest.php | 47 +++++++++++++++++++ 15 files changed, 94 insertions(+), 23 deletions(-) diff --git a/public/index.php b/public/index.php index f7512c6..733cadb 100644 --- a/public/index.php +++ b/public/index.php @@ -77,7 +77,7 @@ if ($method === 'POST' && $path != 'setup') { if (!Session::isValid($_POST['csrf_token'])) { // Invalid session - redirect to /login Log::info('Attempt to POST with invalid session. Redirecting to login.'); - header('Location: ' . $config->basePath . '/login'); + header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login')); exit; } } else { diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php index a0898a3..d0f17ef 100644 --- a/src/Controller/AdminController/AdminController.php +++ b/src/Controller/AdminController/AdminController.php @@ -30,7 +30,7 @@ class AdminController extends Controller { public function handleSave(){ if (!Session::isLoggedIn()){ - header('Location: ' . $config->basePath . '/login'); + header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login')); exit; } diff --git a/src/Controller/AuthController/AuthController.php b/src/Controller/AuthController/AuthController.php index 72fe40e..1ab8189 100644 --- a/src/Controller/AuthController/AuthController.php +++ b/src/Controller/AuthController/AuthController.php @@ -30,7 +30,7 @@ class AuthController extends Controller { Log::info("Successful login for {$username}"); Session::newLoginSession($user); - header('Location: ' . $config->basePath); + header('Location: ' . Util::buildRelativeUrl($config->basePath)); exit; } else { Log::warning("Failed login for {$username}"); @@ -48,7 +48,7 @@ class AuthController extends Controller { Session::end(); global $config; - header('Location: ' . $config->basePath); + header('Location: ' . Util::buildRelativeUrl($config->basePath)); exit; } } \ No newline at end of file diff --git a/src/Controller/EmojiController/EmojiController.php b/src/Controller/EmojiController/EmojiController.php index ad03ee7..07520c9 100644 --- a/src/Controller/EmojiController/EmojiController.php +++ b/src/Controller/EmojiController/EmojiController.php @@ -29,7 +29,7 @@ break; } - header('Location: ' . $config->basePath . 'admin/emoji'); + header('Location: ' . Util::buildRelativeUrl($config->basePath, 'admin/emoji')); exit; } diff --git a/src/Controller/HomeController/HomeController.php b/src/Controller/HomeController/HomeController.php index e0f8c1e..f417540 100644 --- a/src/Controller/HomeController/HomeController.php +++ b/src/Controller/HomeController/HomeController.php @@ -39,7 +39,7 @@ class HomeController extends Controller { global $config; // redirect to the index (will show the latest tick if one was sent) - header('Location: ' . $config->basePath); + header('Location: ' . Util::buildRelativeUrl($config->basePath)); exit; } diff --git a/src/Controller/MoodController/MoodController.php b/src/Controller/MoodController/MoodController.php index c15cfae..f586ae2 100644 --- a/src/Controller/MoodController/MoodController.php +++ b/src/Controller/MoodController/MoodController.php @@ -35,7 +35,7 @@ $user = $user->save(); // go back to the index and show the updated mood - header('Location: ' . $config->basePath); + header('Location: ' . Util::buildRelativeUrl($config->basePath)); exit; } } diff --git a/src/Framework/Util/Util.php b/src/Framework/Util/Util.php index 20a912e..9456cb3 100644 --- a/src/Framework/Util/Util.php +++ b/src/Framework/Util/Util.php @@ -79,4 +79,28 @@ class Util { return $baseUrl . $basePath . $path; } + + public static function buildRelativeUrl(string $basePath, string $path = ''): string { + // Ensure basePath starts with / for relative URLs + $basePath = '/' . ltrim($basePath, '/'); + + // Remove trailing slash unless it's just '/' + if ($basePath !== '/') { + $basePath = rtrim($basePath, '/'); + } + + // Add path + $path = ltrim($path, '/'); + + if ($path === '') { + return $basePath; + } + + // If basePath is root, don't add extra slash + if ($basePath === '/') { + return '/' . $path; + } + + return $basePath . '/' . $path; + } } \ No newline at end of file diff --git a/templates/main.php b/templates/main.php index 6c6bf7c..0045384 100644 --- a/templates/main.php +++ b/templates/main.php @@ -10,10 +10,10 @@ + href="basePath, 'css/default.css')) ?>"> cssId)): ?> + href="basePath, 'css/custom/' . $config->customCssFilename())) ?>"> SetupAdmin
diff --git a/templates/partials/css.php b/templates/partials/css.php index 91826fd..75b1084 100644 --- a/templates/partials/css.php +++ b/templates/partials/css.php @@ -2,7 +2,7 @@

CSS Management

- +
Manage diff --git a/templates/partials/emoji.php b/templates/partials/emoji.php index 790baa4..7f97f6d 100644 --- a/templates/partials/emoji.php +++ b/templates/partials/emoji.php @@ -2,7 +2,7 @@

Emoji Management

- +
Add Emoji @@ -24,7 +24,7 @@
-
+
Delete Emoji diff --git a/templates/partials/home.php b/templates/partials/home.php index 120b5c7..ec9b52a 100644 --- a/templates/partials/home.php +++ b/templates/partials/home.php @@ -14,7 +14,7 @@ strictAccessibility): ?>tabindex="0" - href="basePath) ?>mood" + href="basePath, 'mood')) ?>" class="change-mood">Change mood diff --git a/templates/partials/login.php b/templates/partials/login.php index fe2994e..32d2f1f 100644 --- a/templates/partials/login.php +++ b/templates/partials/login.php @@ -2,7 +2,7 @@

Login

- +
diff --git a/templates/partials/navbar.php b/templates/partials/navbar.php index 3ef8cb7..ed2bafb 100644 --- a/templates/partials/navbar.php +++ b/templates/partials/navbar.php @@ -2,32 +2,32 @@ \ No newline at end of file diff --git a/tests/Framework/Util/UtilTest.php b/tests/Framework/Util/UtilTest.php index 0b1e3b7..e4bdfb5 100644 --- a/tests/Framework/Util/UtilTest.php +++ b/tests/Framework/Util/UtilTest.php @@ -26,4 +26,51 @@ final class UtilTest extends TestCase $this->assertSame($relativeTime, $display); } + public static function buildUrlProvider(): array { + return [ + 'basic path' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'], + 'baseUrl with trailing slash' => ['https://example.com/', 'tkr', 'admin', 'https://example.com/tkr/admin'], + 'empty basePath' => ['https://example.com', '', 'admin', 'https://example.com/admin'], + 'root basePath' => ['https://example.com', '/', 'admin', 'https://example.com/admin'], + 'basePath no leading slash' => ['https://example.com', 'tkr', 'admin', 'https://example.com/tkr/admin'], + 'basePath with leading slash' => ['https://example.com', '/tkr', 'admin', 'https://example.com/tkr/admin'], + 'basePath with trailing slash' => ['https://example.com', 'tkr/', 'admin', 'https://example.com/tkr/admin'], + 'basePath with both slashes' => ['https://example.com', '/tkr/', 'admin', 'https://example.com/tkr/admin'], + 'complex path' => ['https://example.com', 'tkr', 'admin/css/upload', 'https://example.com/tkr/admin/css/upload'], + 'path with leading slash' => ['https://example.com', 'tkr', '/admin', 'https://example.com/tkr/admin'], + 'no path - empty basePath' => ['https://example.com', '', '', 'https://example.com/'], + 'no path - root basePath' => ['https://example.com', '/', '', 'https://example.com/'], + 'no path - tkr basePath' => ['https://example.com', 'tkr', '', 'https://example.com/tkr/'], + ]; + } + + #[DataProvider('buildUrlProvider')] + public function testBuildUrl(string $baseUrl, string $basePath, string $path, string $expected): void { + $result = Util::buildUrl($baseUrl, $basePath, $path); + $this->assertEquals($expected, $result); + } + + public static function buildRelativeUrlProvider(): array { + return [ + 'empty basePath with path' => ['', 'admin', '/admin'], + 'root basePath with path' => ['/', 'admin', '/admin'], + 'tkr basePath with path' => ['tkr', 'admin', '/tkr/admin'], + 'tkr with leading slash' => ['/tkr', 'admin', '/tkr/admin'], + 'tkr with trailing slash' => ['tkr/', 'admin', '/tkr/admin'], + 'tkr with both slashes' => ['/tkr/', 'admin', '/tkr/admin'], + 'complex path' => ['tkr', 'admin/css/upload', '/tkr/admin/css/upload'], + 'path with leading slash' => ['tkr', '/admin', '/tkr/admin'], + 'no path - empty basePath' => ['', '', '/'], + 'no path - root basePath' => ['/', '', '/'], + 'no path - tkr basePath' => ['tkr', '', '/tkr'], + 'no path - tkr with slashes' => ['/tkr/', '', '/tkr'], + ]; + } + + #[DataProvider('buildRelativeUrlProvider')] + public function testBuildRelativeUrl(string $basePath, string $path, string $expected): void { + $result = Util::buildRelativeUrl($basePath, $path); + $this->assertEquals($expected, $result); + } + } \ No newline at end of file