Move small tasks from wishlist to todo, update npm packages Replace #[Auth] attributes with manual auth checks and code quality rule Remove on_jqhtml_ready lifecycle method from framework Complete ACL system with 100-based role indexing and /dev/acl tester WIP: ACL system implementation with debug instrumentation Convert rsx:check JS linting to RPC socket server Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature Reorganize wishlists: priority order, mark sublayouts complete, add email Update model_fetch docs: mark MVP complete, fix enum docs, reorganize Comprehensive documentation overhaul: clarity, compression, and critical rules Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null() Add JS ORM relationship lazy-loading and fetch array handling Add JS ORM relationship fetching and CRUD documentation Fix ORM hydration and add IDE resolution for Base_* model stubs Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework Enhance JS ORM infrastructure and add Json_Tree class name badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
277 lines
8.7 KiB
PHP
Executable File
277 lines
8.7 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\CodeQuality\Rules\PHP;
|
|
|
|
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
|
|
|
/**
|
|
* ModelFetchAuthCheckRule - Validates that model fetch() methods have auth checks
|
|
*
|
|
* When a model has a fetch() method with the #[Ajax_Endpoint_Model_Fetch] attribute,
|
|
* it becomes callable from JavaScript via Orm_Controller. The fetch() method is
|
|
* responsible for implementing its own authorization checks.
|
|
*
|
|
* This rule ensures that fetch() methods in Rsx_Model_Abstract subclasses
|
|
* contain auth check patterns when they have the Ajax_Endpoint_Model_Fetch attribute.
|
|
*
|
|
* Valid auth check patterns:
|
|
* - Session::is_logged_in()
|
|
* - Session::get_user()
|
|
* - Permission::has_permission()
|
|
* - Permission::has_role()
|
|
* - response_unauthorized()
|
|
* - ->site_id check (verifies ownership)
|
|
*
|
|
* Exemption:
|
|
* - Add @auth-exempt comment with reason for public data fetch
|
|
*/
|
|
class ModelFetchAuthCheck_CodeQualityRule extends CodeQualityRule_Abstract
|
|
{
|
|
/**
|
|
* Get the unique rule identifier
|
|
*/
|
|
public function get_id(): string
|
|
{
|
|
return 'PHP-MODEL-FETCH-01';
|
|
}
|
|
|
|
/**
|
|
* Get human-readable rule name
|
|
*/
|
|
public function get_name(): string
|
|
{
|
|
return 'Model Fetch Authentication Check';
|
|
}
|
|
|
|
/**
|
|
* Get rule description
|
|
*/
|
|
public function get_description(): string
|
|
{
|
|
return 'Validates that model fetch() methods with #[Ajax_Endpoint_Model_Fetch] have authentication checks';
|
|
}
|
|
|
|
/**
|
|
* Get file patterns this rule applies to
|
|
*/
|
|
public function get_file_patterns(): array
|
|
{
|
|
return ['*_model.php', '*_Model.php'];
|
|
}
|
|
|
|
/**
|
|
* Whether this rule is called during manifest scan
|
|
*/
|
|
public function is_called_during_manifest_scan(): bool
|
|
{
|
|
return false; // Only run during rsx:check
|
|
}
|
|
|
|
/**
|
|
* Get default severity for this rule
|
|
*/
|
|
public function get_default_severity(): string
|
|
{
|
|
return 'high';
|
|
}
|
|
|
|
/**
|
|
* Patterns that indicate an auth check is present
|
|
*/
|
|
private const AUTH_CHECK_PATTERNS = [
|
|
'Session::is_logged_in',
|
|
'Session::get_user',
|
|
'Session::get_user_id',
|
|
'Session::get_site_id',
|
|
'Permission::has_permission',
|
|
'Permission::has_role',
|
|
'response_unauthorized',
|
|
'->has_permission(',
|
|
'->has_role(',
|
|
'->site_id', // Checking ownership via site_id
|
|
'get_site_id()', // Session site check
|
|
];
|
|
|
|
/**
|
|
* Check a file for violations
|
|
*/
|
|
public function check(string $file_path, string $contents, array $metadata = []): void
|
|
{
|
|
// Read original file content (not sanitized) for comment checking
|
|
$original_contents = file_get_contents($file_path);
|
|
|
|
// Skip if file-level exception comment is present
|
|
if (strpos($original_contents, '@' . $this->get_id() . '-EXCEPTION') !== false) {
|
|
return;
|
|
}
|
|
|
|
// Only check model files (must extend Rsx_Model_Abstract)
|
|
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Rsx_Model_Abstract') {
|
|
return;
|
|
}
|
|
|
|
// Skip archived files
|
|
if (str_contains($file_path, '/archive/') || str_contains($file_path, '/archived/')) {
|
|
return;
|
|
}
|
|
|
|
// Get the class name
|
|
$class_name = $metadata['class'] ?? null;
|
|
if (!$class_name) {
|
|
return;
|
|
}
|
|
|
|
// Get public static methods from metadata
|
|
$methods = $metadata['public_static_methods'] ?? [];
|
|
|
|
// Check if fetch() method exists
|
|
if (!isset($methods['fetch'])) {
|
|
return;
|
|
}
|
|
|
|
$fetch_info = $methods['fetch'];
|
|
$attributes = $fetch_info['attributes'] ?? [];
|
|
|
|
// Check if fetch has Ajax_Endpoint_Model_Fetch attribute
|
|
$has_fetch_attribute = false;
|
|
foreach ($attributes as $attr_name => $attr_data) {
|
|
$short_name = basename(str_replace('\\', '/', $attr_name));
|
|
if ($short_name === 'Ajax_Endpoint_Model_Fetch') {
|
|
$has_fetch_attribute = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Skip if fetch doesn't have the attribute (not exposed via ORM)
|
|
if (!$has_fetch_attribute) {
|
|
return;
|
|
}
|
|
|
|
// Get line number for fetch method
|
|
$line_number = $fetch_info['line'] ?? 1;
|
|
|
|
// Check if method has @auth-exempt comment
|
|
if ($this->method_has_auth_exempt($original_contents, 'fetch', $line_number)) {
|
|
return;
|
|
}
|
|
|
|
// Check if method body has auth check
|
|
$method_body = $this->extract_method_body($contents, 'fetch');
|
|
if ($method_body && $this->body_has_auth_check($method_body)) {
|
|
return;
|
|
}
|
|
|
|
// Violation found - no auth check
|
|
$code_snippet = "#[Ajax_Endpoint_Model_Fetch]\npublic static function fetch(\$id)";
|
|
|
|
$this->add_violation(
|
|
$file_path,
|
|
$line_number,
|
|
"Model fetch() method has no authentication check",
|
|
$code_snippet,
|
|
$this->build_suggestion($class_name),
|
|
'high'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a code body has an auth check pattern
|
|
*/
|
|
private function body_has_auth_check(string $body): bool
|
|
{
|
|
foreach (self::AUTH_CHECK_PATTERNS as $pattern) {
|
|
if (str_contains($body, $pattern)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a method has @auth-exempt comment
|
|
*/
|
|
private function method_has_auth_exempt(string $contents, string $method_name, int $method_line): bool
|
|
{
|
|
$lines = explode("\n", $contents);
|
|
|
|
// Check the 10 lines before the method definition for @auth-exempt
|
|
$start_line = max(0, $method_line - 11);
|
|
$end_line = $method_line - 1;
|
|
|
|
for ($i = $start_line; $i <= $end_line && $i < count($lines); $i++) {
|
|
$line = $lines[$i];
|
|
if (str_contains($line, '@auth-exempt')) {
|
|
return true;
|
|
}
|
|
// Stop if we hit another method definition
|
|
if (preg_match('/^\s*public\s+static\s+function\s+/', $line) && !str_contains($line, $method_name)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract method body from file contents
|
|
*/
|
|
private function extract_method_body(string $contents, string $method_name): ?string
|
|
{
|
|
// Pattern to match method definition
|
|
$pattern = '/public\s+static\s+function\s+' . preg_quote($method_name, '/') . '\s*\([^)]*\)[^{]*\{/s';
|
|
|
|
if (!preg_match($pattern, $contents, $matches, PREG_OFFSET_CAPTURE)) {
|
|
return null;
|
|
}
|
|
|
|
$start_pos = $matches[0][1] + strlen($matches[0][0]) - 1;
|
|
$brace_count = 1;
|
|
$pos = $start_pos + 1;
|
|
$length = strlen($contents);
|
|
|
|
while ($pos < $length && $brace_count > 0) {
|
|
$char = $contents[$pos];
|
|
if ($char === '{') {
|
|
$brace_count++;
|
|
} elseif ($char === '}') {
|
|
$brace_count--;
|
|
}
|
|
$pos++;
|
|
}
|
|
|
|
return substr($contents, $start_pos, $pos - $start_pos);
|
|
}
|
|
|
|
/**
|
|
* Build suggestion for fixing the violation
|
|
*/
|
|
private function build_suggestion(string $class_name): string
|
|
{
|
|
$suggestions = [];
|
|
$suggestions[] = "Model fetch() method needs an authentication/authorization check.";
|
|
$suggestions[] = "";
|
|
$suggestions[] = "Option 1: Check user is logged in and verify ownership:";
|
|
$suggestions[] = " #[Ajax_Endpoint_Model_Fetch]";
|
|
$suggestions[] = " public static function fetch(\$id)";
|
|
$suggestions[] = " {";
|
|
$suggestions[] = " if (!Session::is_logged_in()) {";
|
|
$suggestions[] = " return null; // or response_unauthorized()";
|
|
$suggestions[] = " }";
|
|
$suggestions[] = " \$record = static::find(\$id);";
|
|
$suggestions[] = " if (!\$record || \$record->site_id !== Session::get_site_id()) {";
|
|
$suggestions[] = " return null; // Wrong site or not found";
|
|
$suggestions[] = " }";
|
|
$suggestions[] = " return \$record;";
|
|
$suggestions[] = " }";
|
|
$suggestions[] = "";
|
|
$suggestions[] = "Option 2: If this is intentionally public data, add @auth-exempt:";
|
|
$suggestions[] = " /**";
|
|
$suggestions[] = " * @auth-exempt Public reference data (countries, etc.)";
|
|
$suggestions[] = " */";
|
|
$suggestions[] = " #[Ajax_Endpoint_Model_Fetch]";
|
|
$suggestions[] = " public static function fetch(\$id) { ... }";
|
|
|
|
return implode("\n", $suggestions);
|
|
}
|
|
}
|