Files
rspade_system/app/Database/RestrictedEloquentBuilder.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

259 lines
7.7 KiB
PHP
Executable File

<?php
namespace App\Database;
use Illuminate\Database\Eloquent\Builder;
/**
* Custom Eloquent query builder that prevents eager loading and unsafe operations
*
* This builder overrides dangerous methods to enforce RSpade framework safety rules:
* - All eager loading methods throw exceptions (with/withCount/etc)
* - DELETE without WHERE clause throws exception to prevent accidental mass deletion
*/
class RestrictedEloquentBuilder extends Builder
{
/**
* Prevent eager loading via with()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function with($relations, $callback = null)
{
// Allow empty with() calls (Laravel uses these internally)
if (empty($relations)) {
return parent::with($relations, $callback);
}
// Also allow if relations is an empty array
if (is_array($relations) && count($relations) === 0) {
return parent::with($relations, $callback);
}
// Throw exception for actual eager loading attempts
throw new \RuntimeException(
'Eager loading via with() is not allowed in the RSpade framework. ' .
'Use explicit queries for each relationship instead. ' .
'Attempted to eager load: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via withCount()
*
* @param mixed $relations
* @return $this
* @throws \RuntimeException
*/
public function withCount($relations)
{
// Allow empty withCount() calls
if (empty($relations)) {
return parent::withCount($relations);
}
throw new \RuntimeException(
'Eager loading counts via withCount() is not allowed in the RSpade framework. ' .
'Use explicit count queries instead. ' .
'Attempted to eager load counts for: ' . $this->format_relations($relations)
);
}
/**
* Prevent eager loading via withMax()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withMax($relation, $column)
{
// Allow empty withMax() calls
if (empty($relation)) {
return parent::withMax($relation, $column);
}
throw new \RuntimeException(
'Eager loading max via withMax() is not allowed in the RSpade framework. ' .
'Use explicit max queries instead.'
);
}
/**
* Prevent eager loading via withMin()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withMin($relation, $column)
{
// Allow empty withMin() calls
if (empty($relation)) {
return parent::withMin($relation, $column);
}
throw new \RuntimeException(
'Eager loading min via withMin() is not allowed in the RSpade framework. ' .
'Use explicit min queries instead.'
);
}
/**
* Prevent eager loading via withSum()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withSum($relation, $column)
{
// Allow empty withSum() calls
if (empty($relation)) {
return parent::withSum($relation, $column);
}
throw new \RuntimeException(
'Eager loading sum via withSum() is not allowed in the RSpade framework. ' .
'Use explicit sum queries instead.'
);
}
/**
* Prevent eager loading via withAvg()
*
* @param array|string $relation
* @param string $column
* @return $this
* @throws \RuntimeException
*/
public function withAvg($relation, $column)
{
// Allow empty withAvg() calls
if (empty($relation)) {
return parent::withAvg($relation, $column);
}
throw new \RuntimeException(
'Eager loading avg via withAvg() is not allowed in the RSpade framework. ' .
'Use explicit avg queries instead.'
);
}
/**
* Prevent eager loading via withExists()
*
* @param array|string $relation
* @return $this
* @throws \RuntimeException
*/
public function withExists($relation)
{
// Allow empty withExists() calls
if (empty($relation)) {
return parent::withExists($relation);
}
throw new \RuntimeException(
'Eager loading exists via withExists() is not allowed in the RSpade framework. ' .
'Use explicit exists queries instead.'
);
}
/**
* Prevent eager loading via withAggregate()
*
* @param mixed $relations
* @param string $column
* @param string $function
* @return $this
* @throws \RuntimeException
*/
public function withAggregate($relations, $column, $function = null)
{
// Allow empty withAggregate() calls
if (empty($relations)) {
return parent::withAggregate($relations, $column, $function);
}
throw new \RuntimeException(
'Eager loading aggregates via withAggregate() is not allowed in the RSpade framework. ' .
'Use explicit aggregate queries instead.'
);
}
/**
* Prevent lazy eager loading via has()
* Note: has() is different - it's for filtering, not loading
* But we'll still prevent it if it tries to eager load
*
* @param string $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @param \Closure|null $callback
* @return $this
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?\Closure $callback = null)
{
// has() is allowed as it's for filtering, not eager loading
// But log it for monitoring
if (app()->environment() !== 'testing') {
logger()->debug('has() query used on relation', [
'model' => get_class($this->model),
'relation' => $relation,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5)
]);
}
return parent::has($relation, $operator, $count, $boolean, $callback);
}
/**
* Override delete to prevent deleting all records without WHERE clause
*
* @param mixed $id
* @return int
* @throws \RuntimeException
*/
public function delete($id = null)
{
// If $id is provided, allow it (deleting by primary key)
if ($id !== null) {
return parent::delete($id);
}
// Check if there are any WHERE clauses on the underlying query
$baseQuery = $this->getQuery();
if (empty($baseQuery->wheres)) {
// @PHP-DB-01-EXCEPTION - DB::table() mentioned in error message, not used
shouldnt_happen(
'Attempted to delete all records from ' . $this->getModel()->getTable() . ' without WHERE clause. ' .
'This operation is forbidden to prevent accidental data loss. ' .
'If you truly need to delete all records, use DB::table()->truncate() or add a WHERE clause.'
);
}
return parent::delete();
}
/**
* Format relations for error messages
*
* @param mixed $relations
* @return string
*/
protected function format_relations($relations)
{
if (is_array($relations)) {
return implode(', ', array_keys($relations));
}
return (string) $relations;
}
}