Better static analysis

This commit is contained in:
Lance Pioch 2024-03-17 12:52:22 -04:00
parent 53c1626805
commit 3cea8ca979
28 changed files with 214 additions and 69 deletions

View File

@ -34,7 +34,6 @@ class EggScriptController extends Controller
$copy = Egg::query()
->whereNull('copy_script_from')
->where('nest_id', $egg->nest_id)
->whereNot('id', $egg->id)
->firstOrFail();

View File

@ -31,7 +31,7 @@ class NodeViewController extends Controller
*/
public function index(Request $request, Node $node): View
{
$node->load('location')->loadCount('servers');
$node->loadCount('servers');
$stats = Node::query()
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
@ -61,7 +61,7 @@ class NodeViewController extends Controller
return view('admin.nodes.view.index', [
'node' => $node,
'stats' => $stats,
'stats' => $usageStats,
'version' => $this->versionService,
]);
}

View File

@ -136,13 +136,13 @@ class UserController extends Controller
// Handle single user requests.
if ($request->query('user_id')) {
$user = User::query()->findOrFail($request->input('user_id'));
$user->md5 = md5(strtolower($user->email));
$user['md5'] = md5(strtolower($user->email));
return $user;
}
return $users->map(function ($item) {
$item->md5 = md5(strtolower($item->email));
return $users->map(function (User $item) {
$item['md5'] = md5(strtolower($item->email));
return $item;
});

View File

@ -16,9 +16,9 @@ class ActivityLogController extends ClientApiController
public function __invoke(ClientApiRequest $request): array
{
$activity = QueryBuilder::for($request->user()->activity())
->with('actor')
->allowedFilters([AllowedFilter::partial('event')])
->allowedSorts(['timestamp'])
->with('actor')
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->paginate(min($request->query('per_page', 25), 100))
->appends($request->query());

View File

@ -24,9 +24,9 @@ class ActivityLogController extends ClientApiController
$this->authorize(Permission::ACTION_ACTIVITY_READ, $server);
$activity = QueryBuilder::for($server->activity())
->with('actor')
->allowedSorts(['timestamp'])
->allowedFilters([AllowedFilter::partial('event')])
->with('actor')
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
// We could do this with a query and a lot of joins, but that gets pretty

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Models\ActivityLogSubject;
use App\Models\Backup;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
@ -48,7 +50,7 @@ class ServerDetailsController extends Controller
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
// within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables', 'location')
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables')
->where('node_id', $node->id)
// If you don't cast this to a string you'll end up with a stringified per_page returned in
// the metadata, and then daemon will panic crash as a result.
@ -90,15 +92,19 @@ class ServerDetailsController extends Controller
foreach ($servers as $server) {
/** @var \App\Models\ActivityLog|null $activity */
$activity = $server->activity->first();
if (!is_null($activity)) {
if ($subject = $activity->subjects->where('subject_type', 'backup')->first()) {
// Just create a new audit entry for this event and update the server state
// so that power actions, file management, and backups can resume as normal.
Activity::event('server:backup.restore-failed')
->subject($server, $subject->subject)
->property('name', $subject->subject->name)
->log();
}
if (!$activity) {
continue;
}
if ($subject = $activity->subjects()->where('subject_type', 'backup')->first()) {
/** @var Backup $backup */
$backup = $subject->subject;
// Just create a new audit entry for this event and update the server state
// so that power actions, file management, and backups can resume as normal.
Activity::event('server:backup.restore-failed')
->subject($server, $backup)
->property('name', $backup->name)
->log();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Hashing\Hasher;
@ -72,6 +73,7 @@ class ResetPasswordController extends Controller
*/
protected function resetPassword($user, $password)
{
/** @var User $user */
$user->password = $this->hasher->make($password);
$user->setRememberToken(Str::random(60));
$user->save();

View File

@ -69,6 +69,7 @@ class ResourceBelongsToServer
// Tasks are special since they're (currently) the only item in the API
// that requires something in addition to the server in order to be accessed.
case Task::class:
/** @var Schedule $schedule */
$schedule = $request->route()->parameter('schedule');
if ($model->schedule_id !== $schedule->id || $schedule->server_id !== $server->id) {
throw $exception;

View File

@ -12,7 +12,9 @@ class MountFormRequest extends AdminFormRequest
public function rules(): array
{
if ($this->method() === 'PATCH') {
return Mount::getRulesForUpdate($this->route()->parameter('mount')->id);
/** @var Mount $mount */
$mount = $this->route()->parameter('mount');
return Mount::getRulesForUpdate($mount->id);
}
return Mount::getRules();

View File

@ -37,6 +37,7 @@ abstract class ApplicationApiRequest extends FormRequest
throw new PanelException('An ACL resource must be defined on API requests.');
}
/** @var ApiKey $token */
$token = $this->user()->currentAccessToken();
if ($token instanceof TransientToken) {
return true;

View File

@ -12,8 +12,9 @@ class UpdateNodeRequest extends StoreNodeRequest
*/
public function rules(array $rules = null): array
{
$node = $this->route()->parameter('node')->id;
/** @var Node $node */
$node = $this->route()->parameter('node');
return parent::rules(Node::getRulesForUpdate($node));
return parent::rules(Node::getRulesForUpdate($node->id));
}
}

View File

@ -21,6 +21,7 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
*/
public function rules(): array
{
/** @var Server $server */
$server = $this->route()->parameter('server');
return [

View File

@ -63,7 +63,6 @@ abstract class SubuserRequest extends ClientApiRequest
// Otherwise, get the current subuser's permission set, and ensure that the
// permissions they are trying to assign are not _more_ than the ones they
// already have.
/** @var \App\Models\Subuser|null $subuser */
/** @var \App\Services\Servers\GetUserPermissionsService $service */
$service = $this->container->make(GetUserPermissionsService::class);

View File

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* \App\Models\ActivityLogSubject.
@ -37,7 +38,9 @@ class ActivityLogSubject extends Pivot
public function subject()
{
$morph = $this->morphTo();
if (method_exists($morph, 'withTrashed')) {
if (in_array(SoftDeletes::class, class_uses_recursive($morph::class))) {
/** @var self|Backup|UserSSHKey $morph - cannot use traits in doc blocks */
return $morph->withTrashed();
}

View File

@ -8,12 +8,12 @@ use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Validation\Rule;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Exceptions\Model\DataValidationException;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Validation\Validator;
abstract class Model extends IlluminateModel
{
@ -71,16 +71,6 @@ abstract class Model extends IlluminateModel
return 'uuid';
}
/**
* Set the model to skip validation when saving.
*/
public function skipValidation(): self
{
$this->skipValidation = true;
return $this;
}
/**
* Returns the validator instance used by this model.
*/
@ -88,7 +78,7 @@ abstract class Model extends IlluminateModel
{
$rules = $this->exists ? static::getRulesForUpdate($this) : static::getRules();
return static::$validatorFactory->make([], $rules, [], []);
return static::$validatorFactory->make([], $rules);
}
/**
@ -97,7 +87,7 @@ abstract class Model extends IlluminateModel
public static function getRules(): array
{
$rules = static::$validationRules;
foreach ($rules as $key => &$rule) {
foreach ($rules as &$rule) {
$rule = is_array($rule) ? $rule : explode('|', $rule);
}

View File

@ -59,8 +59,8 @@ class Node extends Model
*/
protected $hidden = ['daemon_token_id', 'daemon_token'];
private int $sum_memory;
private int $sum_disk;
public int $sum_memory;
public int $sum_disk;
/**
* Cast values to correct type.
@ -228,7 +228,7 @@ class Node extends Model
return collect($map)->only(['id', 'ip', 'port']);
});
$item->ports = $filtered->map(function ($map) {
$ports = $filtered->map(function ($map) {
return [
'id' => $map['id'],
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
@ -238,7 +238,7 @@ class Node extends Model
return [
'id' => $item->id,
'text' => $item->name,
'allocations' => $item->ports,
'allocations' => $ports,
];
})->values();
}

View File

@ -393,7 +393,7 @@ class Server extends Model
*/
public function getBackupsGeneratedDuringTimespan(int $seconds = 600): array|Collection
{
return self::query()
return $this
->backups()
->where(fn ($query) => $query->whereNull('completed_at')->orWhere('is_successful', true))
->where('created_at', '>=', now()->subSeconds($seconds))

View File

@ -25,7 +25,7 @@ class ServerInstalled extends Notification implements ShouldQueue
* Handle a direct call to this notification from the server installed event. This is configured
* in the event service provider.
*/
public function handle(Event|Installed $event): void
public function handle(Installed $event): void
{
$event->server->loadMissing('user');

View File

@ -8,14 +8,14 @@ class EggVariableObserver
{
public function creating(EggVariable $variable): void
{
if ($variable->field_type) {
if (isset($variable->field_type)) {
unset($variable->field_type);
}
}
public function updating(EggVariable $variable): void
{
if ($variable->field_type) {
if (isset($variable->field_type)) {
unset($variable->field_type);
}
}

View File

@ -24,7 +24,7 @@ abstract class DaemonRepository
/**
* Set the server model this request is stemming from.
*/
public function setServer(Server $server): self
public function setServer(Server $server): static
{
$this->server = $server;
@ -36,7 +36,7 @@ abstract class DaemonRepository
/**
* Set the node model this request is stemming from.
*/
public function setNode(Node $node): self
public function setNode(Node $node): static
{
$this->node = $node;

View File

@ -2,6 +2,8 @@
namespace App\Services\Backups;
use App\Extensions\Filesystem\S3Filesystem;
use Aws\S3\S3Client;
use Illuminate\Http\Response;
use App\Models\Backup;
use GuzzleHttp\Exception\ClientException;
@ -70,10 +72,13 @@ class DeleteBackupService
$this->connection->transaction(function () use ($backup) {
$backup->delete();
/** @var \App\Extensions\Filesystem\S3Filesystem $adapter */
/** @var S3Filesystem $adapter */
$adapter = $this->manager->adapter(Backup::ADAPTER_AWS_S3);
$adapter->getClient()->deleteObject([
/** @var S3Client $client */
$client = $adapter->getClient();
$client->deleteObject([
'Bucket' => $adapter->getBucket(),
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
]);

View File

@ -114,7 +114,7 @@ class InitiateBackupService
'ignored_files' => array_values($this->ignoredFiles ?? []),
'disk' => $this->backupManager->getDefaultAdapter(),
'is_locked' => $this->isLocked,
], true, true);
]);
$this->daemonBackupRepository->setServer($server)
->setBackupAdapter($this->backupManager->getDefaultAdapter())

View File

@ -56,7 +56,7 @@ class AllocationSelectionService
// Ranges are stored in the ports array as an array which can be
// better processed in the repository.
if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) {
if (abs($matches[2] - $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) {
if (abs((int) $matches[2] - (int) $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) {
throw new DisplayException(trans('exceptions.allocations.too_many_ports'));
}

View File

@ -9,24 +9,13 @@ class EggUpdateService
{
/**
* Update an egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Repository\RecordNotFoundException
* @throws \App\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function handle(Egg $egg, array $data): void
{
$eggId = array_get($data, 'config_from');
if ($eggId) {
$results = Egg::query()
->where('nest_id', $egg->nest_id)
->where('id', $eggId)
->count();
$copiedFromEgg = Egg::query()->find($eggId);
if ($results !== 1) {
throw new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id'));
}
}
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
// TODO: Once the admin UI is done being reworked and this is exposed
// in said UI, remove this so that you can actually update the denylist.

View File

@ -12,7 +12,7 @@ class NodeTransformer extends BaseTransformer
/**
* List of resources that can be included.
*/
protected array $availableIncludes = ['allocations', 'location', 'servers'];
protected array $availableIncludes = ['allocations', 'servers'];
/**
* Return the resource name for the JSONAPI output.

View File

@ -22,7 +22,6 @@ class ServerTransformer extends BaseTransformer
'subusers',
'egg',
'variables',
'location',
'node',
'databases',
'transfer',

146
phpstan-baseline.neon Normal file
View File

@ -0,0 +1,146 @@
parameters:
ignoreErrors:
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:danger\\(\\)\\.$#"
count: 1
path: app/Exceptions/DisplayException.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/ApiController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:danger\\(\\)\\.$#"
count: 2
path: app/Http/Controllers/Admin/DatabaseController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 3
path: app/Http/Controllers/Admin/DatabaseController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 3
path: app/Http/Controllers/Admin/Eggs/EggController.php
-
message: "#^Access to an undefined property App\\\\Models\\\\Egg\\:\\:\\$nest_id\\.$#"
count: 1
path: app/Http/Controllers/Admin/Eggs/EggScriptController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Eggs/EggScriptController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 2
path: app/Http/Controllers/Admin/Eggs/EggShareController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 3
path: app/Http/Controllers/Admin/Eggs/EggVariableController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 4
path: app/Http/Controllers/Admin/MountController.php
-
message: "#^Access to private property App\\\\Models\\\\Node\\:\\:\\$sum_disk\\.$#"
count: 1
path: app/Http/Controllers/Admin/Nodes/NodeViewController.php
-
message: "#^Access to private property App\\\\Models\\\\Node\\:\\:\\$sum_memory\\.$#"
count: 1
path: app/Http/Controllers/Admin/Nodes/NodeViewController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:info\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/NodesController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 4
path: app/Http/Controllers/Admin/NodesController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Servers/CreateServerController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:warning\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Servers/CreateServerController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:danger\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Servers/ServerTransferController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Servers/ServerTransferController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 9
path: app/Http/Controllers/Admin/ServersController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Settings/AdvancedController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Admin/Settings/IndexController.php
-
message: "#^Access to an undefined property App\\\\Models\\\\User\\|Illuminate\\\\Database\\\\Eloquent\\\\Collection\\<int, App\\\\Models\\\\User\\>\\:\\:\\$md5\\.$#"
count: 1
path: app/Http/Controllers/Admin/UserController.php
-
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#"
count: 1
path: app/Http/Controllers/Admin/UserController.php
-
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$md5\\.$#"
count: 1
path: app/Http/Controllers/Admin/UserController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:success\\(\\)\\.$#"
count: 2
path: app/Http/Controllers/Admin/UserController.php
-
message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\:\\:allowedFilters\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Api/Client/ActivityLogController.php
-
message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\:\\:allowedSorts\\(\\)\\.$#"
count: 1
path: app/Http/Controllers/Api/Client/Servers/ActivityLogController.php
-
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$name\\.$#"
count: 1
path: app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php
-
message: "#^Call to an undefined method Prologue\\\\Alerts\\\\AlertsMessageBag\\:\\:danger\\(\\)\\.$#"
count: 1
path: app/Http/Middleware/RequireTwoFactorAuthentication.php

View File

@ -7,11 +7,12 @@ parameters:
- app/
# Level 9 is the highest level
level: 1
level: 2
ignoreErrors:
# Prologue\Alerts defines its methods from its configuration file dynamically
- '#^Call to an undefined method Prologue\\Alerts\\AlertsMessageBag::(danger|success|info|warning)\(\)\.$#'
# ignoreErrors:
# - '#PHPDoc tag @var#'
#
# excludePaths:
# - ./*/*/FileToBeExcluded.php
#