4 changed files with 106 additions and 8 deletions
@ -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; |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue