Browse Source

UnfoldBundle: theme assets

imwald
Nuša Pukšič 2 days ago
parent
commit
6ed2d01fa7
  1. 5
      config/services.yaml
  2. 72
      src/UnfoldBundle/Controller/ThemeAssetController.php
  3. 10
      src/UnfoldBundle/Resources/config/routes.yaml
  4. 27
      src/UnfoldBundle/Theme/HandlebarsRenderer.php

5
config/services.yaml

@ -148,6 +148,11 @@ services: @@ -148,6 +148,11 @@ services:
App\UnfoldBundle\Controller\SiteController:
tags: ['controller.service_arguments']
App\UnfoldBundle\Controller\ThemeAssetController:
arguments:
$projectDir: '%kernel.project_dir%'
tags: ['controller.service_arguments']
App\UnfoldBundle\Controller\UnfoldAdminController:
tags: ['controller.service_arguments']

72
src/UnfoldBundle/Controller/ThemeAssetController.php

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
<?php
namespace App\UnfoldBundle\Controller;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Mime\MimeTypes;
/**
* Serves static assets from theme directories
*/
class ThemeAssetController
{
private readonly string $themesBasePath;
private readonly MimeTypes $mimeTypes;
public function __construct(
private readonly string $projectDir,
) {
$this->themesBasePath = $projectDir . '/src/UnfoldBundle/Resources/themes';
$this->mimeTypes = new MimeTypes();
}
public function __invoke(Request $request, string $theme, string $path): Response
{
// Sanitize theme name - only allow alphanumeric, dash, underscore
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $theme)) {
throw new NotFoundHttpException('Invalid theme name');
}
// Prevent directory traversal
$path = ltrim($path, '/');
if (str_contains($path, '..') || str_starts_with($path, '/')) {
throw new NotFoundHttpException('Invalid path');
}
// Build full file path
$filePath = $this->themesBasePath . '/' . $theme . '/assets/' . $path;
// Verify file exists and is within theme directory
$realPath = realpath($filePath);
$realThemePath = realpath($this->themesBasePath . '/' . $theme);
if ($realPath === false || $realThemePath === false) {
throw new NotFoundHttpException('Asset not found');
}
if (!str_starts_with($realPath, $realThemePath)) {
throw new NotFoundHttpException('Invalid asset path');
}
if (!is_file($realPath)) {
throw new NotFoundHttpException('Asset not found');
}
// Determine content type
$extension = pathinfo($realPath, PATHINFO_EXTENSION);
$mimeType = $this->mimeTypes->getMimeTypes($extension)[0] ?? 'application/octet-stream';
$response = new BinaryFileResponse($realPath);
$response->headers->set('Content-Type', $mimeType);
// Cache for 1 week in production
$response->setMaxAge(604800);
$response->setPublic();
return $response;
}
}

10
src/UnfoldBundle/Resources/config/routes.yaml

@ -1,6 +1,16 @@ @@ -1,6 +1,16 @@
# Unfold Bundle Routes
# Mount these routes for subdomain-based site rendering
# Theme assets route - must come before catch-all
unfold_theme_assets:
path: /assets/themes/{theme}/{path}
controller: App\UnfoldBundle\Controller\ThemeAssetController
requirements:
theme: '[a-zA-Z0-9_-]+'
path: '.+'
methods: [GET]
# Main site controller - catch-all
unfold_site:
path: /{path}
controller: App\UnfoldBundle\Controller\SiteController

27
src/UnfoldBundle/Theme/HandlebarsRenderer.php

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
namespace App\UnfoldBundle\Theme;
use LightnCandy\Flags;
use LightnCandy\LightnCandy;
use Psr\Log\LoggerInterface;
@ -86,6 +87,9 @@ class HandlebarsRenderer @@ -86,6 +87,9 @@ class HandlebarsRenderer
$this->setTheme($theme);
}
// Add asset path prefix to context for runtime use
$context['@assetPath'] = '/assets/themes/' . $this->currentTheme;
$renderer = $this->getCompiledTemplate($templateName);
try {
@ -188,7 +192,7 @@ class HandlebarsRenderer @@ -188,7 +192,7 @@ class HandlebarsRenderer
/**
* Compile template in memory (fallback)
*/
private function compileInMemory(string $templateFile): callable
private function compileInMemory(string $templateFile): \Closure
{
if (!file_exists($templateFile)) {
// Return a basic fallback renderer
@ -198,14 +202,19 @@ class HandlebarsRenderer @@ -198,14 +202,19 @@ class HandlebarsRenderer
$template = file_get_contents($templateFile);
$phpCode = LightnCandy::compile($template, [
'flags' => LightnCandy::FLAG_HANDLEBARS
| LightnCandy::FLAG_ERROR_EXCEPTION
| LightnCandy::FLAG_RUNTIMEPARTIAL,
'flags' => Flags::FLAG_HANDLEBARS
| Flags::FLAG_ERROR_EXCEPTION
| Flags::FLAG_RUNTIMEPARTIAL,
'partials' => $this->loadPartials(),
'helpers' => $this->getHelpers(),
]);
return LightnCandy::prepare($phpCode);
$tmpFile = tempnam(sys_get_temp_dir(), 'lc_');
file_put_contents($tmpFile, '<?php return ' . $phpCode . ';');
$renderer = require $tmpFile;
unlink($tmpFile);
return $renderer;
}
/**
@ -247,9 +256,11 @@ class HandlebarsRenderer @@ -247,9 +256,11 @@ class HandlebarsRenderer
return '/' . ltrim($path, '/');
},
// Asset URL helper
'asset' => function ($path) {
return '/themes/default/assets/' . ltrim($path, '/');
// Asset URL helper - uses @assetPath from runtime context
'asset' => function ($path, $options = null) {
// Get asset path from context (passed in render method)
$assetPath = $options['data']['root']['@assetPath'] ?? '/assets/themes/default';
return $assetPath . '/' . ltrim($path, '/');
},
// Truncate helper

Loading…
Cancel
Save