Files
rspade_system/app/RSpade/CodeQuality/Rules/JavaScript/DirectAjaxApi_CodeQualityRule.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

144 lines
4.9 KiB
PHP

<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DirectAjaxApi_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-AJAX-01';
}
public function get_name(): string
{
return 'Direct AJAX API Call Check';
}
public function get_description(): string
{
return "Detects direct $.ajax calls to /_ajax/ endpoints instead of using JS controller stubs";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript files for direct $.ajax calls to /_ajax/ endpoints
* Instead of:
* await $.ajax({ url: '/_ajax/Controller/action', ... })
* Should use:
* await Controller.action(params)
* Or:
* await Ajax.call(Rsx.Route('Controller::action'), params)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip Ajax.js itself
if (str_ends_with($file_path, '/Ajax.js')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ or app/RSpade/
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
// Pattern to match $.ajax({ url: '/_ajax/Controller/action'
// This handles both single-line and multi-line cases
$full_content = implode("\n", $lines);
// Match $.ajax({ with optional whitespace/newlines, then url: with quotes around /_ajax/
// Capture controller and action names for suggestion
$pattern = '/\$\.ajax\s*\(\s*\{[^}]*?url\s*:\s*[\'"](\/_ajax\/([A-Za-z_][A-Za-z0-9_]*)\/([A-Za-z_][A-Za-z0-9_]*))[^\'"]*[\'"]/s';
if (preg_match_all($pattern, $full_content, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$matched_text = $match[0];
$offset = $match[1];
$url = $matches[1][$index][0];
$controller = $matches[2][$index][0];
$action = $matches[3][$index][0];
// Find line number
$line_number = substr_count(substr($full_content, 0, $offset), "\n") + 1;
// Get the actual line for display
$line = $lines[$line_number - 1] ?? '';
// Build suggestion
$suggestion = "Instead of direct $.ajax() call to '{$url}', use:\n";
$suggestion .= " 1. Preferred: await {$controller}.{$action}(params)\n";
$suggestion .= " 2. Alternative: await Ajax.call('{$controller}', '{$action}', params)\n";
$suggestion .= "The JS stub handles session expiry, notifications, and response unwrapping.";
$this->add_violation(
$file_path,
$line_number,
"Direct $.ajax() call to internal API endpoint '{$url}' detected. Use JS controller stub instead.",
trim($line),
$suggestion,
'high'
);
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}