folderweb/app/router.php
Ruben 90ae20aea3 Remove asset handling from language extraction section
Move asset handling to after language extraction

The asset handling code was previously located in the language
extraction section, which could interfere with proper language
detection. This commit moves the asset handling code to after the
language extraction logic, ensuring that language detection works
correctly before any asset handling occurs.
2025-10-02 23:56:16 +02:00

488 lines
18 KiB
PHP

<?php
// Load configuration
$configFile = file_exists(__DIR__ . '/../custom/config.ini')
? __DIR__ . '/../custom/config.ini'
: __DIR__ . '/config.ini';
$config = parse_ini_file($configFile, true);
$defaultLang = $config['languages']['default'] ?? 'no';
$availableLangs = array_map('trim', explode(',', $config['languages']['available'] ?? 'no'));
$contentDir = realpath($_SERVER['DOCUMENT_ROOT']);
$requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/';
$hasTrailingSlash = str_ends_with($requestUri, '/') && $requestUri !== '/';
$requestPath = trim($requestUri, '/');
// Extract language from URL
$currentLang = $defaultLang;
$pathParts = explode('/', $requestPath);
if (!empty($pathParts[0]) && in_array($pathParts[0], $availableLangs) && $pathParts[0] !== $defaultLang) {
$currentLang = $pathParts[0];
array_shift($pathParts);
$requestPath = implode('/', $pathParts);
}
// Determine if this is a docs request
$isDocsRequest = str_starts_with($requestPath, 'docs');
// Use docs templates for /docs, otherwise use custom/default templates
if ($isDocsRequest) {
$baseTemplate = __DIR__ . '/default/docs/templates/base.php';
$pageTemplate = __DIR__ . '/default/docs/templates/base.php';
$listTemplate = __DIR__ . '/default/docs/templates/list.php';
} else {
$customBaseTemplate = dirname(__DIR__) . '/custom/templates/base.php';
$defaultBaseTemplate = __DIR__ . '/default/templates/base.php';
$baseTemplate = file_exists($customBaseTemplate) ? $customBaseTemplate : $defaultBaseTemplate;
$customPageTemplate = dirname(__DIR__) . '/custom/templates/page.php';
$defaultPageTemplate = __DIR__ . '/default/templates/page.php';
$pageTemplate = file_exists($customPageTemplate) ? $customPageTemplate : $defaultPageTemplate;
$customListTemplate = dirname(__DIR__) . '/custom/templates/list.php';
$defaultListTemplate = __DIR__ . '/default/templates/list.php';
$listTemplate = file_exists($customListTemplate) ? $customListTemplate : $defaultListTemplate;
}
// Build file patterns with language variants
function buildFilePatterns(string $lang, string $defaultLang): array {
$extensions = ['php', 'html', 'md'];
$patterns = ['page' => [], 'single' => []];
foreach ($extensions as $ext) {
// Language-specific files first (if not default language)
if ($lang !== $defaultLang) {
$patterns['page'][] = "page.$lang.$ext";
$patterns['single'][] = "single.$lang.$ext";
$patterns['single'][] = "post.$lang.$ext";
$patterns['single'][] = "article.$lang.$ext";
}
// Default files
$patterns['page'][] = "page.$ext";
$patterns['single'][] = "single.$ext";
$patterns['single'][] = "post.$ext";
$patterns['single'][] = "article.$ext";
}
return $patterns;
}
$pageFilePatterns = buildFilePatterns($currentLang, $defaultLang);
function findMatchingFile(string $dir, array $patterns): ?string {
foreach ($patterns as $pattern) {
if (file_exists($file = "$dir/$pattern")) return $file;
}
return null;
}
function resolveTranslatedPath(string $requestPath, string $contentDir, string $lang, string $defaultLang): string {
// If default language, no translation needed
if ($lang === $defaultLang) {
return $requestPath;
}
$parts = explode('/', trim($requestPath, '/'));
$resolvedParts = [];
$currentPath = $contentDir;
foreach ($parts as $segment) {
if (empty($segment)) continue;
// Check all subdirectories for slug matches
$found = false;
if (is_dir($currentPath)) {
$subdirs = array_filter(
scandir($currentPath) ?: [],
fn($item) => !in_array($item, ['.', '..']) && is_dir("$currentPath/$item")
);
foreach ($subdirs as $dir) {
$metadata = loadMetadata("$currentPath/$dir", $lang, $defaultLang);
if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) {
$resolvedParts[] = $dir;
$currentPath .= "/$dir";
$found = true;
break;
}
}
}
// If no slug match, use segment as-is
if (!$found) {
$resolvedParts[] = $segment;
$currentPath .= "/$segment";
}
}
return implode('/', $resolvedParts);
}
function parseRequestPath(string $requestPath, string $contentDir, array $patterns, bool $hasTrailingSlash, string $lang, string $defaultLang): array {
// Resolve translated slugs to actual directory names
$resolvedPath = resolveTranslatedPath($requestPath, $contentDir, $lang, $defaultLang);
$contentPath = rtrim($contentDir, '/') . '/' . ltrim($resolvedPath, '/');
if (is_file($contentPath)) {
return ['type' => 'file', 'path' => realpath($contentPath)];
}
if (is_dir($contentPath)) {
// Check if directory has subdirectories
$hasSubdirs = !empty(array_filter(
scandir($contentPath) ?: [],
fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentPath/$item")
));
// If directory has subdirectories, treat as directory (for list views)
// Otherwise, if it has a content file, treat as file
if (!$hasSubdirs && ($file = findMatchingFile($contentPath, $patterns['single']) ?: findMatchingFile($contentPath, $patterns['page']))) {
return ['type' => 'file', 'path' => $file, 'needsSlash' => !$hasTrailingSlash];
}
return ['type' => 'directory', 'path' => realpath($contentPath)];
}
return ['type' => 'not_found', 'path' => $contentPath];
}
function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array {
$metadataFile = "$dirPath/metadata.ini";
if (!file_exists($metadataFile)) return null;
$metadata = parse_ini_file($metadataFile, true);
if (!$metadata) return null;
// Extract base metadata (non-section values)
$baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY);
// If current language is not default, merge language-specific overrides
if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) {
$baseMetadata = array_merge($baseMetadata, $metadata[$lang]);
}
return $baseMetadata ?: null;
}
function extractTitle(string $filePath, array $patterns): ?string {
$file = findMatchingFile($filePath, $patterns['single']) ?: findMatchingFile($filePath, $patterns['page']);
if (!$file) return null;
$ext = pathinfo($file, PATHINFO_EXTENSION);
$content = file_get_contents($file);
if ($ext === 'md' && preg_match('/^#\s+(.+)$/m', $content, $matches)) {
return trim($matches[1]);
}
if (in_array($ext, ['html', 'php']) && preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
return strip_tags($matches[1]);
}
return null;
}
function formatNorwegianDate(string $dateString): string {
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $dateString, $matches)) {
$months = ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'];
$day = (int)$matches[3];
$month = $months[(int)$matches[2] - 1];
$year = $matches[1];
return "$day. $month $year";
}
return $dateString;
}
function extractDateFromFolder(string $folderName): ?string {
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})-/', $folderName, $matches)) {
return formatNorwegianDate($matches[1] . '-' . $matches[2] . '-' . $matches[3]);
}
return null;
}
function findCoverImage(string $dirPath): ?string {
$extensions = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
foreach ($extensions as $ext) {
if (file_exists("$dirPath/cover.$ext")) {
return "cover.$ext";
}
}
return null;
}
function loadTranslations(string $lang): array {
$translationFile = dirname(__DIR__) . "/custom/languages/$lang.ini";
if (file_exists($translationFile)) {
return parse_ini_file($translationFile) ?: [];
}
return [];
}
function buildNavigation(string $contentDir, string $currentLang, string $defaultLang, array $pageFilePatterns): array {
$navItems = [];
// Scan top-level directories in content
$items = array_filter(
scandir($contentDir) ?: [],
fn($item) => !in_array($item, ['.', '..']) && is_dir("$contentDir/$item")
);
foreach ($items as $item) {
$itemPath = "$contentDir/$item";
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
// Check if this item should be in menu
if (!$metadata || empty($metadata['menu'])) {
continue;
}
// Check if content exists for current language
if ($currentLang !== $defaultLang) {
$extensions = ['php', 'html', 'md'];
$hasContent = false;
// Check for language-specific content files
foreach ($extensions as $ext) {
if (file_exists("$itemPath/single.$currentLang.$ext") ||
file_exists("$itemPath/post.$currentLang.$ext") ||
file_exists("$itemPath/article.$currentLang.$ext") ||
file_exists("$itemPath/page.$currentLang.$ext")) {
$hasContent = true;
break;
}
}
// If no language-specific files, check if metadata has title for this language
if (!$hasContent && $metadata && isset($metadata['title'])) {
$hasContent = true;
}
if (!$hasContent) continue;
}
// Extract title and build URL
$title = $metadata['title'] ?? extractTitle($itemPath, $pageFilePatterns) ?? ucfirst($item);
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
// Use translated slug if available
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug']
: $item;
$navItems[] = [
'title' => $title,
'url' => $langPrefix . '/' . urlencode($urlSlug) . '/',
'order' => (int)($metadata['menu_order'] ?? 999)
];
}
// Sort by menu_order
usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']);
return $navItems;
}
function renderTemplate(string $content, int $statusCode = 200): void {
global $baseTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
// Build navigation for templates
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
// Load frontpage metadata for home button label
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
$homeLabel = $frontpageMetadata['slug'] ?? 'Home';
// Load translations
$translations = loadTranslations($currentLang);
http_response_code($statusCode);
include $baseTemplate;
exit;
}
function renderFile(string $filePath): void {
global $baseTemplate, $pageTemplate, $contentDir, $currentLang, $defaultLang, $pageFilePatterns;
$realPath = realpath($filePath);
if (!$realPath || !str_starts_with($realPath, $contentDir) || !is_readable($realPath)) {
renderTemplate("<article><h1>403 Forbidden</h1><p>Access denied.</p></article>", 403);
}
$ext = pathinfo($realPath, PATHINFO_EXTENSION);
if (in_array($ext, ['php', 'html', 'md'])) {
ob_start();
if ($ext === 'md') {
if (!class_exists('Parsedown')) {
require_once __DIR__ . '/vendor/Parsedown.php';
}
echo '<article>' . (new Parsedown())->text(file_get_contents($realPath)) . '</article>';
} else {
include $realPath;
}
$content = ob_get_clean();
// Build navigation for templates
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
// Load metadata for current page/directory
$pageDir = dirname($realPath);
$pageMetadata = loadMetadata($pageDir, $currentLang, $defaultLang);
$pageTitle = $pageMetadata['title'] ?? null;
// Load frontpage metadata for home button label
$frontpageMetadata = loadMetadata($contentDir, $currentLang, $defaultLang);
$homeLabel = $frontpageMetadata['slug'] ?? 'Home';
// Load translations
$translations = loadTranslations($currentLang);
// Wrap content with page template
ob_start();
include $pageTemplate;
$content = ob_get_clean();
// Wrap with base template
include $baseTemplate;
exit;
}
// Serve other file types directly
header('Content-Type: ' . (mime_content_type($realPath) ?: 'application/octet-stream'));
readfile($realPath);
exit;
}
// Check for assets in /custom/assets/ served at root level
$assetPath = dirname(__DIR__) . '/custom/assets/' . $requestPath;
if (file_exists($assetPath) && is_file($assetPath)) {
header('Content-Type: ' . (mime_content_type($assetPath) ?: 'application/octet-stream'));
readfile($assetPath);
exit;
}
// Handle frontpage
if (empty($requestPath)) {
// Try language-specific frontpage first, then default
$frontPage = null;
if ($currentLang !== $defaultLang && file_exists("$contentDir/frontpage.$currentLang.php")) {
$frontPage = "$contentDir/frontpage.$currentLang.php";
} elseif (file_exists("$contentDir/frontpage.php")) {
$frontPage = "$contentDir/frontpage.php";
}
if ($frontPage) {
renderFile($frontPage);
}
}
// Parse and handle request
$parsedPath = parseRequestPath($requestPath, $contentDir, $pageFilePatterns, $hasTrailingSlash, $currentLang, $defaultLang);
switch ($parsedPath['type']) {
case 'file':
// Redirect to add trailing slash if this is a directory-based page
if (!empty($parsedPath['needsSlash'])) {
header('Location: ' . rtrim($_SERVER['REQUEST_URI'], '/') . '/', true, 301);
exit;
}
renderFile($parsedPath['path']);
case 'directory':
$dir = $parsedPath['path'];
if (file_exists("$dir/index.php")) {
renderFile("$dir/index.php");
}
// Check for page content file in this directory
$pageContent = null;
if ($pageFile = findMatchingFile($dir, $pageFilePatterns['page'])) {
$ext = pathinfo($pageFile, PATHINFO_EXTENSION);
ob_start();
if ($ext === 'md') {
if (!class_exists('Parsedown')) {
require_once __DIR__ . '/vendor/Parsedown.php';
}
echo (new Parsedown())->text(file_get_contents($pageFile));
} else {
include $pageFile;
}
$pageContent = ob_get_clean();
}
// Load metadata for this directory
$metadata = loadMetadata($dir, $currentLang, $defaultLang);
// Check for custom list template (prefer list-grid.php over list.php)
$customListGridTemplate = dirname(__DIR__) . '/custom/templates/list-grid.php';
if (file_exists($customListGridTemplate)) {
$listTemplate = $customListGridTemplate;
}
// Default directory listing
$subdirs = array_filter(
scandir($dir) ?: [],
fn($item) => !in_array($item, ['.', '..']) && is_dir("$dir/$item")
);
$items = array_filter(array_map(function($item) use ($dir, $requestPath, $currentLang, $defaultLang, $pageFilePatterns) {
$itemPath = "$dir/$item";
// Check if content exists for current language
if ($currentLang !== $defaultLang) {
// For non-default languages, only check language-specific files
$extensions = ['php', 'html', 'md'];
$hasContent = false;
foreach ($extensions as $ext) {
if (file_exists("$itemPath/single.$currentLang.$ext") ||
file_exists("$itemPath/post.$currentLang.$ext") ||
file_exists("$itemPath/article.$currentLang.$ext") ||
file_exists("$itemPath/page.$currentLang.$ext")) {
$hasContent = true;
break;
}
}
if (!$hasContent) return null;
}
// Build patterns for rendering (includes fallbacks)
$patterns = buildFilePatterns($currentLang, $defaultLang);
$metadata = loadMetadata($itemPath, $currentLang, $defaultLang);
$coverImage = findCoverImage($itemPath);
$title = $metadata['title'] ?? extractTitle($itemPath, $patterns) ?? $item;
$date = null;
if (isset($metadata['date'])) {
$date = formatNorwegianDate($metadata['date']);
} else {
$date = extractDateFromFolder($item) ?: date("F d, Y", filemtime($itemPath));
}
$langPrefix = $currentLang !== $defaultLang ? "/$currentLang" : '';
// Use translated slug if available, otherwise use folder name
$urlSlug = ($currentLang !== $defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug']
: $item;
return [
'title' => $title,
'date' => $date,
'url' => $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug),
'cover' => $coverImage ? $langPrefix . '/' . trim($requestPath, '/') . '/' . urlencode($urlSlug) . '/' . $coverImage : null,
'summary' => $metadata['summary'] ?? null
];
}, $subdirs));
ob_start();
include $listTemplate;
$content = ob_get_clean();
// Build navigation for base template
$navigation = buildNavigation($contentDir, $currentLang, $defaultLang, $pageFilePatterns);
include $baseTemplate;
exit;
case 'not_found':
renderTemplate("<h1>404 Not Found</h1><p>The requested resource was not found.</p>", 404);
}