- Fixed the subdirectory loading issue.
Changed: [app/View.php](/Users/tyemeclifford/Documents/GH/blog/app/View.php): generated URLs now honor a base path like /blog. [app/Config.php](/Users/tyemeclifford/Documents/GH/blog/app/Config.php): added base_path support and derives it from base_url or SCRIPT_NAME. [app/App.php](/Users/tyemeclifford/Documents/GH/blog/app/App.php): route parsing strips the base path, and PHP can safely serve theme/upload assets for virtual subdirectory previews. [.htaccess](/Users/tyemeclifford/Documents/GH/blog/.htaccess): removed hardcoded RewriteBase /. [themes/neon/layout.php](/Users/tyemeclifford/Documents/GH/blog/themes/neon/layout.php): root-relative social links like /feed.xml now become /blog/feed.xml. [README.md](/Users/tyemeclifford/Documents/GH/blog/README.md): documented subdirectory setup.
This commit is contained in:
+57
-1
@@ -18,6 +18,7 @@ final class App
|
||||
$this->repository = $repository;
|
||||
$this->comments = $comments;
|
||||
$this->stats = $stats;
|
||||
View::setBasePath($this->config->basePath());
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
@@ -66,6 +67,10 @@ final class App
|
||||
|
||||
private function route(string $path): void
|
||||
{
|
||||
if ($this->servePublicFile($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($path === '' || $path === 'home') {
|
||||
$this->home(1);
|
||||
return;
|
||||
@@ -297,6 +302,54 @@ final class App
|
||||
echo $body;
|
||||
}
|
||||
|
||||
private function servePublicFile(string $path): bool
|
||||
{
|
||||
if (!preg_match('#^(themes/[a-z0-9_-]+/assets/.+|bl-content/uploads/.+)$#', $path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$file = realpath(BLOG_ROOT . '/' . $path);
|
||||
$allowedRoots = [
|
||||
realpath(BLOG_ROOT . '/themes'),
|
||||
realpath(BLOG_ROOT . '/bl-content/uploads'),
|
||||
];
|
||||
$insideAllowedRoot = false;
|
||||
foreach ($allowedRoots as $root) {
|
||||
if (is_string($root) && is_string($file) && str_starts_with($file, $root . DIRECTORY_SEPARATOR)) {
|
||||
$insideAllowedRoot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$insideAllowedRoot || !is_file((string) $file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo((string) $file, PATHINFO_EXTENSION));
|
||||
$types = [
|
||||
'css' => 'text/css; charset=utf-8',
|
||||
'js' => 'application/javascript; charset=utf-8',
|
||||
'svg' => 'image/svg+xml',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
'ico' => 'image/x-icon',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
];
|
||||
|
||||
http_response_code(200);
|
||||
header('Content-Type: ' . ($types[$extension] ?? 'application/octet-stream'));
|
||||
header('Content-Length: ' . filesize((string) $file));
|
||||
header('Cache-Control: public, max-age=3600');
|
||||
$this->stats->record('asset', $path, 200);
|
||||
readfile((string) $file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function path(): string
|
||||
{
|
||||
$route = (string) ($_GET['route'] ?? '');
|
||||
@@ -306,7 +359,10 @@ final class App
|
||||
|
||||
$scriptDir = str_replace('\\', '/', dirname((string) ($_SERVER['SCRIPT_NAME'] ?? '')));
|
||||
$route = '/' . ltrim($route, '/');
|
||||
if ($scriptDir !== '/' && str_starts_with($route, $scriptDir . '/')) {
|
||||
$basePath = $this->config->basePath();
|
||||
if ($basePath !== '' && ($route === $basePath || str_starts_with($route, $basePath . '/'))) {
|
||||
$route = substr($route, strlen($basePath));
|
||||
} elseif ($scriptDir !== '/' && str_starts_with($route, $scriptDir . '/')) {
|
||||
$route = substr($route, strlen($scriptDir));
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,12 @@ final class Config
|
||||
{
|
||||
$configured = trim((string) $this->get('base_url', ''));
|
||||
if ($configured !== '') {
|
||||
$parts = parse_url($configured);
|
||||
if (is_array($parts) && isset($parts['scheme'], $parts['host'])) {
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
return $parts['scheme'] . '://' . $parts['host'] . $port;
|
||||
}
|
||||
|
||||
return rtrim($configured, '/');
|
||||
}
|
||||
|
||||
@@ -96,6 +102,34 @@ final class Config
|
||||
return $scheme . '://' . $host;
|
||||
}
|
||||
|
||||
public function basePath(): string
|
||||
{
|
||||
$configured = trim((string) $this->get('base_path', ''));
|
||||
if ($configured !== '') {
|
||||
return self::normalizeBasePath($configured);
|
||||
}
|
||||
|
||||
$baseUrl = trim((string) $this->get('base_url', ''));
|
||||
if ($baseUrl !== '') {
|
||||
$path = parse_url($baseUrl, PHP_URL_PATH);
|
||||
if (is_string($path) && $path !== '') {
|
||||
return self::normalizeBasePath($path);
|
||||
}
|
||||
}
|
||||
|
||||
if (PHP_SAPI === 'cli') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$scriptName = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
|
||||
$scriptDir = dirname($scriptName);
|
||||
if ($scriptDir === '.' || $scriptDir === '/') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::normalizeBasePath($scriptDir);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private static function defaults(): array
|
||||
{
|
||||
@@ -108,6 +142,7 @@ final class Config
|
||||
'language' => 'en',
|
||||
],
|
||||
'base_url' => '',
|
||||
'base_path' => '',
|
||||
'timezone' => 'UTC',
|
||||
'theme' => 'neon',
|
||||
'posts_per_page' => 6,
|
||||
@@ -124,4 +159,10 @@ final class Config
|
||||
'social' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private static function normalizeBasePath(string $path): string
|
||||
{
|
||||
$path = '/' . trim($path, '/');
|
||||
return $path === '/' ? '' : $path;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -5,6 +5,14 @@ namespace NeonBlog;
|
||||
|
||||
final class View
|
||||
{
|
||||
private static string $basePath = '';
|
||||
|
||||
public static function setBasePath(string $basePath): void
|
||||
{
|
||||
$basePath = '/' . trim($basePath, '/');
|
||||
self::$basePath = $basePath === '/' ? '' : $basePath;
|
||||
}
|
||||
|
||||
public static function e(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
@@ -18,7 +26,12 @@ final class View
|
||||
$path = rtrim($path, '/');
|
||||
}
|
||||
|
||||
return $query === [] ? $path : $path . '?' . http_build_query($query);
|
||||
$url = self::$basePath . $path;
|
||||
if ($url === '') {
|
||||
$url = '/';
|
||||
}
|
||||
|
||||
return $query === [] ? $url : $url . '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $item */
|
||||
|
||||
+9
-1
@@ -23,7 +23,15 @@ spl_autoload_register(static function (string $class): void {
|
||||
}
|
||||
});
|
||||
|
||||
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
|
||||
$isPublicAssetRequest = static function (): bool {
|
||||
$path = (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);
|
||||
$route = (string) ($_GET['route'] ?? '');
|
||||
|
||||
return (bool) preg_match('#/(themes/[a-z0-9_-]+/assets/.+|bl-content/uploads/.+)$#', $path)
|
||||
|| (bool) preg_match('#^(themes/[a-z0-9_-]+/assets/.+|bl-content/uploads/.+)$#', $route);
|
||||
};
|
||||
|
||||
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE && !$isPublicAssetRequest()) {
|
||||
session_start([
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'Lax',
|
||||
|
||||
Reference in New Issue
Block a user