Files
rspade_system/app/RSpade/CodeQuality/Rules/JavaScript/DirectAjaxApi_CodeQualityRule.php
root 9ebcc359ae Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation
Implement underscore prefix for system tables
Fix JS syntax linter to support decorators and grant exception to Task system
SPA: Update planning docs and wishlists with remaining features
SPA: Document Navigation API abandonment and future enhancements
Implement SPA browser integration with History API (Phase 1)
Convert contacts view page to SPA action
Convert clients pages to SPA actions and document conversion procedure
SPA: Merge GET parameters and update documentation
Implement SPA route URL generation in JavaScript and PHP
Implement SPA bootstrap controller architecture
Add SPA routing manual page (rsx:man spa)
Add SPA routing documentation to CLAUDE.md
Phase 4 Complete: Client-side SPA routing implementation
Update get_routes() consumers for unified route structure
Complete SPA Phase 3: PHP-side route type detection and is_spa flag
Restore unified routes structure and Manifest_Query class
Refactor route indexing and add SPA infrastructure
Phase 3 Complete: SPA route registration in manifest
Implement SPA Phase 2: Extract router code and test decorators
Rename Jqhtml_Component to Component and complete SPA foundation setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 17:48:15 +00:00

144 lines
4.9 KiB
PHP
Executable File

<?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;
}
}