Mark PHP version compatibility fallback as legitimate in Php_Fixer

Add public directory asset support to bundle system
Fix PHP Fixer to replace ALL Rsx\ FQCNs with simple class names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 17:18:10 +00:00
parent 6e41df0789
commit 8c8fb8e902
8 changed files with 720 additions and 121 deletions

View File

@@ -9,6 +9,7 @@ namespace App\RSpade\Core\Dispatch;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Redis;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -215,14 +216,88 @@ class AssetHandler
// Set additional security headers
static::__set_security_headers($response, $mime_type);
// Enable gzip if supported
if (static::__should_compress($mime_type)) {
$response->headers->set('Content-Encoding', 'gzip');
}
return $response;
}
/**
* Find a public asset by relative path with Redis caching
*
* Resolves paths like "sneat/css/demo.css" to full filesystem paths like
* "rsx/public/sneat/css/demo.css" by scanning all public/ directories.
*
* Results are cached in Redis indefinitely. Cached paths are validated
* before use - if file no longer exists, cache is invalidated and re-scan occurs.
*
* @param string $relative_path Relative path like "sneat/css/demo.css"
* @return string Full filesystem path
* @throws \Symfony\Component\HttpKernel\Exception\HttpException If not found or ambiguous
*/
public static function find_public_asset(string $relative_path): string
{
// Ensure directories are discovered
static::__ensure_directories_discovered();
// Sanitize the path
$relative_path = static::__sanitize_path($relative_path);
// Check Redis cache first
$cache_key = 'rspade:public_asset:' . $relative_path;
$cached_path = Redis::get($cache_key);
if ($cached_path) {
// Verify cached file still exists
if (File::exists($cached_path) && File::isFile($cached_path)) {
return $cached_path;
}
// Stale cache - invalidate and re-scan
Redis::del($cache_key);
}
// NEVER serve PHP files under any circumstances
$extension = strtolower(pathinfo($relative_path, PATHINFO_EXTENSION));
if ($extension === 'php') {
throw new HttpException(403, 'PHP files cannot be served as static assets');
}
// Scan all public directories for matches
$matches = [];
foreach (static::$public_directories as $module => $directory) {
$full_path = $directory . '/' . $relative_path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules
if (static::__is_file_excluded($full_path, $relative_path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
$matches[] = $full_path;
}
}
// Check for ambiguous matches
if (count($matches) > 1) {
// Show first two matches in error
$first_two = array_slice($matches, 0, 2);
throw new HttpException(
500,
"Ambiguous public asset request: '{$relative_path}' matches multiple files: '" .
implode("', '", $first_two) . "'"
);
}
// Check for no matches
if (count($matches) === 0) {
throw new NotFoundHttpException("Public asset not found: {$relative_path}");
}
// Single match - cache and return
$resolved_path = $matches[0];
Redis::set($cache_key, $resolved_path);
return $resolved_path;
}
/**
* Ensure directories are discovered (lazy initialization)
*/
@@ -294,52 +369,22 @@ class AssetHandler
/**
* Find asset file in public directories
*
* Wrapper around find_public_asset() that returns null instead of throwing
* NotFoundHttpException for backward compatibility with existing code.
*
* @param string $path
* @return string|null Full file path or null if not found
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws \Symfony\Component\HttpKernel\Exception\HttpException For PHP files, exclusions, or ambiguous matches
*/
protected static function __find_asset_file($path)
{
// NEVER serve PHP files under any circumstances
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($extension === 'php') {
throw new HttpException(403, 'PHP files cannot be served as static assets');
try {
return static::find_public_asset($path);
} catch (NotFoundHttpException $e) {
// Not found - return null for backward compatibility
return null;
}
// Try each public directory
foreach (static::$public_directories as $module => $directory) {
$full_path = $directory . '/' . $path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules before returning
if (static::__is_file_excluded($full_path, $path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
return $full_path;
}
}
// Check if path includes module prefix (e.g., "admin/css/style.css")
$parts = explode('/', $path, 2);
if (count($parts) === 2) {
$module = $parts[0];
$asset_path = $parts[1];
if (isset(static::$public_directories[$module])) {
$full_path = static::$public_directories[$module] . '/' . $asset_path;
if (File::exists($full_path) && File::isFile($full_path)) {
// Check exclusion rules before returning
if (static::__is_file_excluded($full_path, $asset_path)) {
throw new HttpException(403, 'Access to this file is forbidden');
}
return $full_path;
}
}
}
return null;
// Let other exceptions (403, 500) bubble up
}
/**
@@ -674,32 +719,6 @@ class AssetHandler
}
}
/**
* Check if content should be compressed
*
* @param string $mime_type
* @return bool
*/
protected static function __should_compress($mime_type)
{
// Compress text-based content
$compressible = [
'text/',
'application/javascript',
'application/json',
'application/xml',
'image/svg+xml'
];
foreach ($compressible as $type) {
if (str_starts_with($mime_type, $type)) {
return true;
}
}
return false;
}
/**
* Get discovered public directories
*