Make URL building more resilient and add tests. (#38)
Some checks failed
Run unit tests / run-unit-tests (push) Has been cancelled
Some checks failed
Run unit tests / run-unit-tests (push) Has been cancelled
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 <greg@subcultureofone.org> Co-committed-by: Greg Sarjeant <greg@subcultureofone.org>
This commit is contained in:
parent
a9f610fc60
commit
8b5a249450
@ -77,7 +77,7 @@ if ($method === 'POST' && $path != 'setup') {
|
|||||||
if (!Session::isValid($_POST['csrf_token'])) {
|
if (!Session::isValid($_POST['csrf_token'])) {
|
||||||
// Invalid session - redirect to /login
|
// Invalid session - redirect to /login
|
||||||
Log::info('Attempt to POST with invalid session. Redirecting 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;
|
exit;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -30,7 +30,7 @@ class AdminController extends Controller {
|
|||||||
|
|
||||||
public function handleSave(){
|
public function handleSave(){
|
||||||
if (!Session::isLoggedIn()){
|
if (!Session::isLoggedIn()){
|
||||||
header('Location: ' . $config->basePath . '/login');
|
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'login'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class AuthController extends Controller {
|
|||||||
Log::info("Successful login for {$username}");
|
Log::info("Successful login for {$username}");
|
||||||
|
|
||||||
Session::newLoginSession($user);
|
Session::newLoginSession($user);
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
Log::warning("Failed login for {$username}");
|
Log::warning("Failed login for {$username}");
|
||||||
@ -48,7 +48,7 @@ class AuthController extends Controller {
|
|||||||
Session::end();
|
Session::end();
|
||||||
|
|
||||||
global $config;
|
global $config;
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,7 +29,7 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ' . $config->basePath . 'admin/emoji');
|
header('Location: ' . Util::buildRelativeUrl($config->basePath, 'admin/emoji'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class HomeController extends Controller {
|
|||||||
global $config;
|
global $config;
|
||||||
|
|
||||||
// redirect to the index (will show the latest tick if one was sent)
|
// redirect to the index (will show the latest tick if one was sent)
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
$user = $user->save();
|
$user = $user->save();
|
||||||
|
|
||||||
// go back to the index and show the updated mood
|
// go back to the index and show the updated mood
|
||||||
header('Location: ' . $config->basePath);
|
header('Location: ' . Util::buildRelativeUrl($config->basePath));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,4 +79,28 @@ class Util {
|
|||||||
|
|
||||||
return $baseUrl . $basePath . $path;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -10,10 +10,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="<?= Util::escape_html($config->basePath) ?>css/default.css">
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/default.css')) ?>">
|
||||||
<?php if (!empty($config->cssId)): ?>
|
<?php if (!empty($config->cssId)): ?>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="<?= Util::escape_html($config->basePath) ?>css/custom/<?= Util::escape_html($config->customCssFilename()) ?>">
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'css/custom/' . $config->customCssFilename())) ?>">
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<link rel="alternate"
|
<link rel="alternate"
|
||||||
type="application/rss+xml"
|
type="application/rss+xml"
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1>
|
<h1><?php if ($isSetup): ?>Setup<?php else: ?>Admin<?php endif; ?></h1>
|
||||||
<main>
|
<main>
|
||||||
<form
|
<form
|
||||||
action="<?php echo $config->basePath . ($isSetup ? 'setup' : 'admin') ?>"
|
action="<?php echo Util::buildRelativeUrl($config->basePath, ($isSetup ? 'setup' : 'admin')) ?>"
|
||||||
method="post">
|
method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<?php /** @var Array $customCss */ ?>
|
<?php /** @var Array $customCss */ ?>
|
||||||
<h1>CSS Management</h1>
|
<h1>CSS Management</h1>
|
||||||
<main>
|
<main>
|
||||||
<form action="<?= $config->basePath ?>admin/css" method="post" enctype="multipart/form-data">
|
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/css') ?>" method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Manage</legend>
|
<legend>Manage</legend>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<?php /** @var array $emojiList */ ?>
|
<?php /** @var array $emojiList */ ?>
|
||||||
<h1>Emoji Management</h1>
|
<h1>Emoji Management</h1>
|
||||||
<main>
|
<main>
|
||||||
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
|
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/emoji') ?>" method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Add Emoji</legend>
|
<legend>Add Emoji</legend>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<?php if (!empty($emojiList)): ?>
|
<?php if (!empty($emojiList)): ?>
|
||||||
<form action="<?= $config->basePath ?>admin/emoji" method="post" enctype="multipart/form-data">
|
<form action="<?= Util::buildRelativeUrl($config->basePath, 'admin/emoji') ?>" method="post" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($_SESSION['csrf_token']) ?>">
|
||||||
<fieldset class="delete-emoji-fieldset">
|
<fieldset class="delete-emoji-fieldset">
|
||||||
<legend>Delete Emoji</legend>
|
<legend>Delete Emoji</legend>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<?php if (Session::isLoggedIn()): ?>
|
<?php if (Session::isLoggedIn()): ?>
|
||||||
<a
|
<a
|
||||||
<?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>mood"
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'mood')) ?>"
|
||||||
class="change-mood">Change mood</a>
|
class="change-mood">Change mood</a>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<?php /** @var string $csrf_token */ ?>
|
<?php /** @var string $csrf_token */ ?>
|
||||||
<?php /** @var string $error */ ?>
|
<?php /** @var string $error */ ?>
|
||||||
<h2>Login</h2>
|
<h2>Login</h2>
|
||||||
<form method="post" action="<?= $config->basePath ?>login">
|
<form method="post" action="<?= Util::buildRelativeUrl($config->basePath, 'login') ?>">
|
||||||
<div class="fieldset-items">
|
<div class="fieldset-items">
|
||||||
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>">
|
<input type="hidden" name="csrf_token" value="<?= Util::escape_html($csrf_token) ?>">
|
||||||
<label for="username">Username:</label>
|
<label for="username">Username:</label>
|
||||||
|
@ -2,32 +2,32 @@
|
|||||||
<?php /* https://www.w3schools.com/howto/howto_css_dropdown.asp */ ?>
|
<?php /* https://www.w3schools.com/howto/howto_css_dropdown.asp */ ?>
|
||||||
<nav aria-label="Main navigation">
|
<nav aria-label="Main navigation">
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>">home</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath)) ?>">home</a>
|
||||||
<details>
|
<details>
|
||||||
<summary aria-haspopup="true">feeds</summary>
|
<summary aria-haspopup="true">feeds</summary>
|
||||||
<div class="dropdown-items">
|
<div class="dropdown-items">
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>feed/rss">rss</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/rss')) ?>">rss</a>
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>feed/atom">atom</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'feed/atom')) ?>">atom</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<?php if (!Session::isLoggedIn()): ?>
|
<?php if (!Session::isLoggedIn()): ?>
|
||||||
<a tabindex="0"
|
<a tabindex="0"
|
||||||
href="<?= Util::escape_html($config->basePath) ?>login">login</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'login')) ?>">login</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<details>
|
<details>
|
||||||
<summary aria-haspopup="true">admin</summary>
|
<summary aria-haspopup="true">admin</summary>
|
||||||
<div class="dropdown-items">
|
<div class="dropdown-items">
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>admin">settings</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin')) ?>">settings</a>
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>admin/css">css</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/css')) ?>">css</a>
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>admin/emoji">emoji</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'admin/emoji')) ?>">emoji</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
<a <?php if($config->strictAccessibility): ?>tabindex="0"<?php endif; ?>
|
||||||
href="<?= Util::escape_html($config->basePath) ?>logout">logout</a>
|
href="<?= Util::escape_html(Util::buildRelativeUrl($config->basePath, 'logout')) ?>">logout</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
@ -26,4 +26,51 @@ final class UtilTest extends TestCase
|
|||||||
$this->assertSame($relativeTime, $display);
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user