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>
259 lines
7.7 KiB
PHP
Executable File
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;
|
|
}
|
|
} |