Fix code quality violations and exclude Manifest from checks
Document application modes (development/debug/production) Add global file drop handler, order column normalization, SPA hash fix Serve CDN assets via /_vendor/ URLs instead of merging into bundles Add production minification with license preservation Improve JSON formatting for debugging and production optimization Add CDN asset caching with CSS URL inlining for production builds Add three-mode system (development, debug, production) Update Manifest CLAUDE.md to reflect helper class architecture Refactor Manifest.php into helper classes for better organization Pre-manifest-refactor checkpoint: Add app_mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,7 +131,7 @@ class AssetHandler
|
||||
|
||||
/**
|
||||
* Check if a path is an asset request
|
||||
*
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
@@ -144,14 +144,21 @@ class AssetHandler
|
||||
return preg_match('/^[A-Za-z0-9_]+__(vendor|app)\.[a-f0-9]{8}\.(js|css)$/', $filename) ||
|
||||
preg_match('/^[A-Za-z0-9_]+__app\.[a-f0-9]{16}\.(js|css)$/', $filename);
|
||||
}
|
||||
|
||||
|
||||
// Check if this is a vendor CDN cache request
|
||||
if (str_starts_with($path, '/_vendor/')) {
|
||||
// Validate filename format: alphanumeric_hash.(js|css)
|
||||
$filename = substr($path, 9); // Remove '/_vendor/'
|
||||
return preg_match('/^[A-Za-z0-9_-]+_[a-f0-9]{12}\.(js|css)$/', $filename);
|
||||
}
|
||||
|
||||
// Check if path has a file extension
|
||||
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
||||
|
||||
|
||||
if (empty($extension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if extension is allowed
|
||||
return in_array(strtolower($extension), static::$allowed_extensions);
|
||||
}
|
||||
@@ -171,6 +178,11 @@ class AssetHandler
|
||||
return static::__serve_compiled_bundle($path, $request);
|
||||
}
|
||||
|
||||
// Handle vendor CDN cache requests
|
||||
if (str_starts_with($path, '/_vendor/')) {
|
||||
return static::__serve_vendor_cdn_cache($path, $request);
|
||||
}
|
||||
|
||||
// Ensure directories are discovered
|
||||
static::__ensure_directories_discovered();
|
||||
|
||||
@@ -677,10 +689,84 @@ class AssetHandler
|
||||
|
||||
// Security headers
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serve a cached CDN vendor file
|
||||
*
|
||||
* Serves files from the CDN cache directory (rsx/resource/.cdn-cache/)
|
||||
* with strict filename validation to prevent path traversal.
|
||||
*
|
||||
* @param string $path The requested path (e.g., /_vendor/lodash_abc123456789.js)
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
protected static function __serve_vendor_cdn_cache($path, Request $request)
|
||||
{
|
||||
// Extract filename from path
|
||||
$filename = substr($path, 9); // Remove '/_vendor/'
|
||||
|
||||
// Strict filename validation - only allow safe characters
|
||||
// Format: name_hash.ext where name is alphanumeric/underscore/hyphen, hash is 12 hex chars
|
||||
if (!preg_match('/^[A-Za-z0-9_-]+_[a-f0-9]{12}\.(js|css)$/', $filename)) {
|
||||
throw new NotFoundHttpException("Invalid vendor filename: {$filename}");
|
||||
}
|
||||
|
||||
// Additional safety checks - no path traversal characters
|
||||
if (str_contains($filename, '..') || str_contains($filename, './') || str_contains($filename, '/')) {
|
||||
Log::warning('Attempted path traversal in vendor request', [
|
||||
'filename' => $filename
|
||||
]);
|
||||
throw new NotFoundHttpException("Invalid vendor filename: {$filename}");
|
||||
}
|
||||
|
||||
// Get the CDN cache directory from the Cdn_Cache class
|
||||
$cache_dir = \App\RSpade\Core\Bundle\Cdn_Cache::get_cache_directory();
|
||||
|
||||
// Build full path - filename is already validated as safe
|
||||
$file_path = $cache_dir . '/' . $filename;
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($file_path)) {
|
||||
throw new NotFoundHttpException("Vendor file not found: {$filename}");
|
||||
}
|
||||
|
||||
// Verify the file is actually in the cache directory (belt and suspenders)
|
||||
// @REALPATH-EXCEPTION - Security: path traversal prevention requires symlink resolution
|
||||
$real_file = realpath($file_path);
|
||||
$real_cache_dir = realpath($cache_dir);
|
||||
|
||||
if (!$real_file || !$real_cache_dir || !str_starts_with($real_file, $real_cache_dir)) {
|
||||
Log::warning('Vendor file path traversal attempt', [
|
||||
'filename' => $filename,
|
||||
'resolved' => $real_file
|
||||
]);
|
||||
throw new NotFoundHttpException("Vendor file not found: {$filename}");
|
||||
}
|
||||
|
||||
// Create binary file response
|
||||
$response = new BinaryFileResponse($file_path);
|
||||
|
||||
// Set appropriate content type
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$mime_type = $extension === 'js' ? 'application/javascript' : 'text/css';
|
||||
$response->headers->set('Content-Type', $mime_type . '; charset=utf-8');
|
||||
|
||||
// Set cache headers - 1 month cache for vendor CDN files
|
||||
// These files are versioned by their content hash in the filename
|
||||
$cache_seconds = 2592000; // 30 days (1 month)
|
||||
$response->headers->set('Cache-Control', 'public, max-age=' . $cache_seconds . ', immutable');
|
||||
$response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $cache_seconds) . ' GMT');
|
||||
|
||||
// Security headers
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a bundle on-demand in development mode
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user