Update phpstan to latest (#804)

* Fix these

* Update phpstan

* Transform these into their identifiers instead

* Fix custom rule

* License is wrong

* Update these

* Pint fixes

* Fix this

* Consolidate these

* Never supported PHP 7

* Better evaluation

* Fixes

* Don’t need ignore

* Replace trait with service

* Subusers are simply the many to many relationship between Servers and Users

* Adjust to remove ignores

* Use new query builder instead!

* wip

* Update composer

* Quick fixes

* Use realtime facade

* Small fixes

* Convert to static to avoid new

* Update to statics

* Don’t modify protected properties directly

* Run pint

* Change to correct method

* Give up and use the facade

* Make sure this route is available

* Filament hasn’t been loaded yet

* This can be readonly

* Typehint

* These are no longer used

* Quick fixes

* Need doc block help

* Always true

* We use caddy with docker

* Pint

* Fix phpstan issues

* Remove unused import

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
Lance Pioch 2025-01-16 14:53:50 -05:00 committed by GitHub
parent 02c4eb19f0
commit ad1a9cd33f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 566 additions and 1324 deletions

View File

@ -1,75 +0,0 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
# If using CentOS this file should be placed in:
# /etc/nginx/conf.d/
#
# The MIT License (MIT)
#
# Pterodactyl®
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
server {
listen 80;
server_name _;
root /app/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# the fastcgi_pass path needs to be changed accordingly when using CentOS
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -1,70 +0,0 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
server {
listen 80;
server_name <domain>;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name <domain>;
root /app/public;
index index.php;
access_log /var/log/nginx/panel.app-access.log;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
# strengthen ssl security
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
# See the link below for more SSL information:
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
#
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
# Add headers to serve security related headers
add_header Strict-Transport-Security "max-age=15768000; preload;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
include /etc/nginx/fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -1,16 +0,0 @@
[www]
user = nginx
group = nginx
listen = 127.0.0.1:9000
listen.owner = nginx
listen.group = nginx
listen.mode = 0750
pm = ondemand
pm.max_children = 9
pm.process_idle_timeout = 10s
pm.max_requests = 200
clear_env = no

View File

@ -6,6 +6,7 @@ use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Throwable;
class ProcessRunnableCommand extends Command
{
@ -13,10 +14,7 @@ class ProcessRunnableCommand extends Command
protected $description = 'Process schedules in the database and determine which are ready to run.';
/**
* Handle command execution.
*/
public function handle(): int
public function handle(ProcessScheduleService $processScheduleService): int
{
$schedules = Schedule::query()
->with('tasks')
@ -35,7 +33,7 @@ class ProcessRunnableCommand extends Command
$bar = $this->output->createProgressBar(count($schedules));
foreach ($schedules as $schedule) {
$bar->clear();
$this->processSchedule($schedule);
$this->processSchedule($processScheduleService, $schedule);
$bar->advance();
$bar->display();
}
@ -50,20 +48,20 @@ class ProcessRunnableCommand extends Command
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*/
protected function processSchedule(Schedule $schedule): void
protected function processSchedule(ProcessScheduleService $processScheduleService, Schedule $schedule): void
{
if ($schedule->tasks->isEmpty()) {
return;
}
try {
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$processScheduleService->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'id' => $schedule->id,
]));
} catch (\Throwable|\Exception $exception) {
} catch (Throwable $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());

View File

@ -19,26 +19,13 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
/**
* BulkPowerActionCommand constructor.
*/
public function __construct(private DaemonPowerRepository $powerRepository, private ValidatorFactory $validator)
{
parent::__construct();
}
/**
* Handle the bulk power request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function handle(): void
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
{
$action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
$servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
$validator = $this->validator->make([
$validator = $validator->make([
'action' => $action,
'nodes' => $nodes,
'servers' => $servers,
@ -64,11 +51,14 @@ class BulkPowerActionCommand extends Command
}
$bar = $this->output->createProgressBar($count);
$powerRepository = $this->powerRepository;
// @phpstan-ignore-next-line
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$bar->clear();
if (!$server instanceof Server) {
return null;
}
try {
$powerRepository->setServer($server)->send($action);
} catch (Exception $exception) {
@ -82,6 +72,8 @@ class BulkPowerActionCommand extends Command
$bar->advance();
$bar->display();
return null;
});
$this->line('');

View File

@ -39,10 +39,6 @@ class UpgradeCommand extends Command
$this->line($this->getUrl());
}
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
$this->error(__('commands.upgrade.php_version') . ' [' . PHP_VERSION . '].');
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {

View File

@ -0,0 +1,24 @@
<?php
namespace App\Eloquent;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends Builder<TModel>
*/
class BackupQueryBuilder extends Builder
{
public function nonFailed(): self
{
$this->where(function (Builder $query) {
$query
->whereNull('completed_at')
->orWhere('is_successful', true);
});
return $this;
}
}

View File

@ -91,7 +91,7 @@ enum EditorLanguages: string implements HasLabel
case yaml = 'yaml';
case json = 'json';
public function getLabel(): ?string
public function getLabel(): string
{
return $this->name;
}

View File

@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class Handler extends ExceptionHandler
{
@ -179,10 +180,7 @@ class Handler extends ExceptionHandler
return response()->json(['errors' => $errors], $exception->status);
}
/**
* Return the exception as a JSONAPI representation for use on API requests.
*/
protected function convertExceptionToArray(\Throwable $e, array $override = []): array
public static function exceptionToArray(Throwable $e, array $override = []): array
{
$match = self::$exceptionResponseCodes[get_class($e)] ?? null;
@ -214,7 +212,7 @@ class Handler extends ExceptionHandler
'trace' => Collection::make($e->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
'previous' => Collection::make($this->extractPrevious($e))
'previous' => Collection::make(self::extractPrevious($e))
->map(fn ($exception) => $exception->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
@ -225,6 +223,14 @@ class Handler extends ExceptionHandler
return ['errors' => [array_merge($error, $override)]];
}
/**
* Return the exception as a JSONAPI representation for use on API requests.
*/
protected function convertExceptionToArray(Throwable $e, array $override = []): array
{
return self::exceptionToArray($e, $override);
}
/**
* Return an array of exceptions that should not be reported.
*/
@ -251,15 +257,12 @@ class Handler extends ExceptionHandler
* Extracts all the previous exceptions that lead to the one passed into this
* function being thrown.
*
* @return \Throwable[]
* @return Throwable[]
*/
protected function extractPrevious(\Throwable $e): array
public static function extractPrevious(Throwable $e): array
{
$previous = [];
while ($value = $e->getPrevious()) {
if (!$value instanceof \Throwable) {
break;
}
$previous[] = $value;
$e = $value;
}
@ -273,7 +276,6 @@ class Handler extends ExceptionHandler
*/
public static function toArray(\Throwable $e): array
{
// @phpstan-ignore-next-line
return (new self(app()))->convertExceptionToArray($e);
return self::exceptionToArray($e);
}
}

View File

@ -1,28 +1,5 @@
<?php
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
namespace App\Extensions\Filesystem;
use Aws\S3\S3ClientInterface;

View File

@ -5,6 +5,7 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Models\User;
use App\Services\Helpers\LanguageService;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Hidden;
@ -40,7 +41,7 @@ class EditUser extends EditRecord
->required()
->hidden()
->default('en')
->options(fn (User $user) => $user->getAvailableLanguages()),
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
Hidden::make('skipValidation')
->default(true),
CheckboxList::make('roles')

View File

@ -34,7 +34,7 @@ class ServersRelationManager extends RelationManager
->label('Suspend All Servers')
->color('warning')
->action(function (SuspensionService $suspensionService) use ($user) {
collect($user->servers()->get())->filter(fn ($server) => !$server->isSuspended())
collect($user->servers)->filter(fn ($server) => !$server->isSuspended())
->each(fn ($server) => $suspensionService->handle($server, SuspendAction::Suspend));
}),
Actions\Action::make('toggleUnsuspend')

View File

@ -10,7 +10,6 @@ use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action
{
@ -55,7 +54,6 @@ class ImportEggAction extends Action
$this->action(function (array $data, EggImporterService $eggImportService): void {
try {
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
foreach ($eggFile as $file) {

View File

@ -10,7 +10,6 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action
{
@ -55,7 +54,6 @@ class ImportEggAction extends Action
$this->action(function (array $data, EggImporterService $eggImportService): void {
try {
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
foreach ($eggFile as $file) {

View File

@ -23,6 +23,6 @@ class DateTimeColumn extends TextColumn
public function getTimezone(): string
{
return auth()->user()?->timezone ?? config('app.timezone', 'UTC');
return auth()->user()->timezone ?? config('app.timezone', 'UTC');
}
}

View File

@ -8,6 +8,7 @@ use App\Facades\Activity;
use App\Models\ActivityLog;
use App\Models\ApiKey;
use App\Models\User;
use App\Services\Helpers\LanguageService;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use App\Services\Users\UserUpdateService;
@ -115,13 +116,12 @@ class EditProfile extends BaseEditProfile
->prefixIcon('tabler-flag')
->live()
->default('en')
->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : "
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : "
Your language ($state) has not been translated yet!
But never fear, you can help fix that by
<a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>.
")
)
->options(fn (User $user) => $user->getAvailableLanguages()),
"))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
]),
Tab::make('OAuth')
@ -208,38 +208,30 @@ class EditProfile extends BaseEditProfile
'addLogoSpace' => true,
'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13,
'version' => Version::AUTO,
// 'outputInterface' => QRSvgWithLogo::class,
'outputBase64' => false,
'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos
'addQuietzone' => true,
// 'drawLightModules' => true,
'connectPaths' => true,
'drawCircularModules' => true,
// 'circleRadius' => 0.45,
'svgDefs' => '
<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/>
<stop stop-color="#0369a1" offset="1"/>
</linearGradient>
<style><![CDATA[
.dark{fill: url(#gradient);}
.light{fill: #000;}
]]></style>
',
]);
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
// QROptions
// @phpstan-ignore property.protected
$options->version = Version::AUTO;
// $options->outputInterface = QRSvgWithLogo::class;
// @phpstan-ignore property.protected
$options->outputBase64 = false;
// @phpstan-ignore property.protected
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
// @phpstan-ignore property.protected
$options->addQuietzone = true;
// $options->drawLightModules = true;
// @phpstan-ignore property.protected
$options->connectPaths = true;
// @phpstan-ignore property.protected
$options->drawCircularModules = true;
// $options->circleRadius = 0.45;
// @phpstan-ignore property.protected
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/>
<stop stop-color="#0369a1" offset="1"/>
</linearGradient>
<style><![CDATA[
.dark{fill: url(#gradient);}
.light{fill: #000;}
]]></style>';
$image = (new QRCode($options))->render($url);
return [

View File

@ -6,7 +6,6 @@ use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
@ -99,7 +98,7 @@ class ListDatabases extends ListRecords
->columnSpan(2)
->required()
->placeholder('Select Database Host')
->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])),
->options(fn () => $server->node->databaseHosts->mapWithKeys(fn ($databaseHost) => [$databaseHost->id => $databaseHost->name])),
TextInput::make('database')
->columnSpan(1)
->label('Database Name')

View File

@ -355,8 +355,7 @@ class ListFiles extends ListRecords
->action(function (Collection $files, $data, DaemonFileRepository $fileRepository) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location']));
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file->name])->toArray();
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file['name']])->toArray();
$fileRepository
->setServer($server)
->renameFiles($this->path, $files);
@ -374,8 +373,7 @@ class ListFiles extends ListRecords
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => $file->name)->toArray();
$files = $files->map(fn ($file) => $file['name'])->toArray();
$fileRepository
->setServer($server)
@ -396,8 +394,7 @@ class ListFiles extends ListRecords
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => $file->name)->toArray();
$files = $files->map(fn ($file) => $file['name'])->toArray();
$fileRepository
->setServer($server)
->deleteFiles($this->path, $files);

View File

@ -30,16 +30,25 @@ class ServerConsole extends Widget
public string $input = '';
private GetUserPermissionsService $getUserPermissionsService;
private NodeJWTService $nodeJWTService;
public function boot(GetUserPermissionsService $getUserPermissionsService, NodeJWTService $nodeJWTService): void
{
$this->getUserPermissionsService = $getUserPermissionsService;
$this->nodeJWTService = $nodeJWTService;
}
protected function getToken(): string
{
if (!$this->user || !$this->server || $this->user->cannot(Permission::ACTION_WEBSOCKET_CONNECT, $this->server)) {
throw new HttpForbiddenException('You do not have permission to connect to this server\'s websocket.');
}
// @phpstan-ignore-next-line
$permissions = app(GetUserPermissionsService::class)->handle($this->server, $this->user);
// @phpstan-ignore-next-line
return app(NodeJWTService::class)
$permissions = $this->getUserPermissionsService->handle($this->server, $this->user);
return $this->nodeJWTService
->setExpiresAt(now()->addMinutes(10)->toImmutable())
->setUser($this->user)
->setClaims([

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Client;
use Illuminate\Auth\SessionGuard;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Auth\AuthManager;
@ -63,8 +64,7 @@ class AccountController extends ClientApiController
// other devices functionality to work.
$guard->setUser($user);
// This method doesn't exist in the stateless Sanctum world.
if (method_exists($guard, 'logoutOtherDevices')) {
if ($guard instanceof SessionGuard) {
$guard->logoutOtherDevices($request->input('password'));
}

View File

@ -22,14 +22,11 @@ use App\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
class BackupController extends ClientApiController
{
/**
* BackupController constructor.
*/
public function __construct(
private DaemonBackupRepository $daemonRepository,
private DeleteBackupService $deleteBackupService,
private InitiateBackupService $initiateBackupService,
private DownloadLinkService $downloadLinkService,
private readonly DaemonBackupRepository $daemonRepository,
private readonly DeleteBackupService $deleteBackupService,
private readonly InitiateBackupService $initiateBackupService,
private readonly DownloadLinkService $downloadLinkService,
) {
parent::__construct();
}
@ -38,7 +35,7 @@ class BackupController extends ClientApiController
* Returns all the backups for a given server instance in a paginated
* result set.
*
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws AuthorizationException
*/
public function index(Request $request, Server $server): array
{

View File

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use App\Models\Server;
use App\Facades\Activity;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Http\Controllers\Api\Client\ClientApiController;
@ -28,7 +27,7 @@ class CommandController extends ClientApiController
$previous = $exception->getPrevious();
if ($previous instanceof BadResponseException) {
if ($previous->getResponse() instanceof ResponseInterface && $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY) {
if ($previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY) {
throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception);
}
}

View File

@ -158,6 +158,6 @@ class SftpAuthenticationController extends Controller
{
$username = explode('.', strrev($request->input('username', '')));
return strtolower(strrev($username[0] ?? '') . '|' . $request->ip());
return strtolower(strrev($username[0]) . '|' . $request->ip());
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Failed;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Event;
use App\Events\Auth\DirectLogin;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
abstract class AbstractLoginController extends Controller
{
use AuthenticatesUsers;
protected AuthManager $auth;
/**
* Lockout time for failed login requests.
*/
protected int $lockoutTime;
/**
* After how many attempts should logins be throttled and locked.
*/
protected int $maxLoginAttempts;
/**
* Where to redirect users after login / registration.
*/
protected string $redirectTo = '/';
/**
* LoginController constructor.
*/
public function __construct()
{
$this->lockoutTime = config('auth.lockout.time');
$this->maxLoginAttempts = config('auth.lockout.attempts');
$this->auth = Container::getInstance()->make(AuthManager::class);
}
/**
* Get the failed login response instance.
*
* @throws \App\Exceptions\DisplayException
*/
protected function sendFailedLoginResponse(Request $request, ?Authenticatable $user = null, ?string $message = null): never
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
$this->getField($request->input('user')) => $request->input('user'),
]);
if ($request->route()->named('auth.login-checkpoint')) {
throw new DisplayException($message ?? trans('auth.two_factor.checkpoint_failed'));
}
throw new DisplayException(trans('auth.failed'));
}
/**
* Send the response after the user was authenticated.
*/
protected function sendLoginResponse(User $user, Request $request): JsonResponse
{
$request->session()->remove('auth_confirmation_token');
$request->session()->regenerate();
$this->clearLoginAttempts($request);
$this->auth->guard()->login($user, true);
Event::dispatch(new DirectLogin($user, true));
return new JsonResponse([
'data' => [
'complete' => true,
'intended' => $this->redirectPath(),
'user' => $user->toReactObject(),
],
]);
}
/**
* Determine if the user is logging in using an email or username.
*/
protected function getField(?string $input = null): string
{
return ($input && str_contains($input, '@')) ? 'email' : 'username';
}
/**
* Fire a failed login event.
*/
protected function fireFailedLoginEvent(?Authenticatable $user = null, array $credentials = []): void
{
Event::dispatch(new Failed('auth', $user, $credentials));
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Password;
use App\Http\Controllers\Controller;
use App\Events\Auth\FailedPasswordReset;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
/**
* Get the response for a failed password reset link.
*/
protected function sendResetLinkFailedResponse(Request $request, string $response): JsonResponse
{
// As noted in #358 we will return success even if it failed
// to avoid pointing out that an account does or does not
// exist on the system.
event(new FailedPasswordReset($request->ip(), $request->input('email')));
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
/**
* Get the response for a successful password reset link.
*/
protected function sendResetLinkResponse(Request $request, string $response): JsonResponse
{
return response()->json([
'status' => trans($response),
]);
}
}

View File

@ -1,124 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Support\Facades\Event;
use App\Events\Auth\ProvidedAuthenticationToken;
use App\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
class LoginCheckpointController extends AbstractLoginController
{
private const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
/**
* LoginCheckpointController constructor.
*/
public function __construct(
private Google2FA $google2FA,
private ValidationFactory $validation
) {
parent::__construct();
}
/**
* Handle a login where the user is required to provide a TOTP authentication
* token. Once a user has reached this stage it is assumed that they have already
* provided a valid username and password.
*
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Exception
* @throws \Illuminate\Validation\ValidationException
*/
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->sendLockoutResponse($request);
}
$details = $request->session()->get('auth_confirmation_token');
if (!$this->hasValidSessionData($details)) {
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
}
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
$this->sendFailedLoginResponse($request);
}
$user = User::query()->find($details['user_id']);
if (!$user) {
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
}
// Recovery tokens go through a slightly different pathway for usage.
if (!is_null($recoveryToken = $request->input('recovery_token'))) {
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
Event::dispatch(new ProvidedAuthenticationToken($user, true));
return $this->sendLoginResponse($user, $request);
}
} else {
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
Event::dispatch(new ProvidedAuthenticationToken($user));
return $this->sendLoginResponse($user, $request);
}
}
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
}
/**
* Determines if a given recovery token is valid for the user account. If we find a matching token
* it will be deleted from the database.
*
* @throws \Exception
*/
protected function isValidRecoveryToken(User $user, string $value): bool
{
foreach ($user->recoveryTokens as $token) {
if (password_verify($value, $token->token)) {
$token->delete();
return true;
}
}
return false;
}
/**
* Determines if the data provided from the session is valid or not. This
* will return false if the data is invalid, or if more time has passed than
* was configured when the session was written.
*/
protected function hasValidSessionData(array $data): bool
{
$validator = $this->validation->make($data, [
'user_id' => 'required|integer|min:1',
'token_value' => 'required|string',
'expires_at' => 'required',
]);
if ($validator->fails()) {
return false;
}
if (!$data['expires_at'] instanceof CarbonInterface) {
return false;
}
if ($data['expires_at']->isBefore(CarbonImmutable::now())) {
return false;
}
return true;
}
}

View File

@ -1,77 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Livewire\Installer\PanelInstaller;
use Carbon\CarbonImmutable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use Illuminate\View\View;
class LoginController extends AbstractLoginController
{
/**
* Handle all incoming requests for the authentication routes and render the
* base authentication view component. React will take over at this point and
* turn the login area into an SPA.
*/
public function index(): View|RedirectResponse
{
if (!PanelInstaller::isInstalled()) {
return redirect('/installer');
}
return view('templates/auth.core');
}
/**
* Handle a login request to the application.
*
* @throws \App\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request): JsonResponse
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
$this->sendLockoutResponse($request);
}
$username = $request->input('user');
$user = User::query()->where($this->getField($username), $username)->first();
if (!$user) {
$this->sendFailedLoginResponse($request);
}
// Ensure that the account is using a valid username and password before trying to
// continue. Previously this was handled in the 2FA checkpoint, however that has
// a flaw in which you can discover if an account exists simply by seeing if you
// can proceed to the next step in the login process.
if (!password_verify($request->input('password'), $user->password)) {
$this->sendFailedLoginResponse($request, $user);
}
if (!$user->use_totp) {
return $this->sendLoginResponse($user, $request);
}
Activity::event('auth:checkpoint')->withRequestMetadata()->subject($user)->log();
$request->session()->put('auth_confirmation_token', [
'user_id' => $user->id,
'token_value' => $token = Str::random(64),
'expires_at' => CarbonImmutable::now()->addMinutes(5),
]);
return new JsonResponse([
'data' => [
'complete' => false,
'confirmation_token' => $token,
],
]);
}
}

View File

@ -16,12 +16,9 @@ use Illuminate\Http\Request;
class OAuthController extends Controller
{
/**
* OAuthController constructor.
*/
public function __construct(
private AuthManager $auth,
private UserUpdateService $updateService
private readonly AuthManager $auth,
private readonly UserUpdateService $updateService
) {}
/**

View File

@ -1,100 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use App\Http\Requests\Auth\ResetPasswordRequest;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* The URL to redirect users to after password reset.
*/
public string $redirectTo = '/';
protected bool $hasTwoFactor = false;
/**
* ResetPasswordController constructor.
*/
public function __construct(
private Hasher $hasher,
) {}
/**
* Reset the given user's password.
*
* @throws \App\Exceptions\DisplayException
*/
public function __invoke(ResetPasswordRequest $request): JsonResponse
{
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise, we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request),
function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($response === Password::PASSWORD_RESET) {
return $this->sendResetResponse();
}
throw new DisplayException(trans($response));
}
/**
* Reset the given user's password. If the user has two-factor authentication enabled on their
* account do not automatically log them in. In those cases, send the user back to the login
* form with a note telling them their password was changed and to log back in.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword|\App\Models\User $user
* @param string $password
*
* @throws \App\Exceptions\Model\DataValidationException
*/
protected function resetPassword($user, $password): void
{
/** @var User $user */
$user->password = $this->hasher->make($password);
$user->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
// If the user is not using 2FA log them in, otherwise skip this step and force a
// fresh login where they'll be prompted to enter a token.
if (!$user->use_totp) {
$this->guard()->login($user);
}
$this->hasTwoFactor = $user->use_totp;
}
/**
* Send a successful password reset response back to the callee.
*/
protected function sendResetResponse(): JsonResponse
{
return response()->json([
'success' => true,
'redirect_to' => $this->redirectTo,
'send_to_login' => $this->hasTwoFactor,
]);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\View\View;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
class IndexController extends Controller
{
/**
* IndexController constructor.
*/
public function __construct(protected ViewFactory $view) {}
/**
* Returns listing of user's servers.
*/
public function index(): View
{
return view('templates/base.core');
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Translation\Translator;
use Illuminate\Contracts\Translation\Loader;
use App\Http\Controllers\Controller;
class LocaleController extends Controller
{
protected Loader $loader;
public function __construct(Translator $translator)
{
$this->loader = $translator->getLoader();
}
/**
* Returns translation data given a specific locale and namespace.
*/
public function __invoke(Request $request): JsonResponse
{
$locales = explode(' ', $request->input('locale') ?? '');
$namespaces = explode(' ', $request->input('namespace') ?? '');
$response = [];
foreach ($locales as $locale) {
$response[$locale] = [];
foreach ($namespaces as $namespace) {
$response[$locale][$namespace] = $this->i18n(
$this->loader->load($locale, str_replace('.', '/', $namespace))
);
}
}
return new JsonResponse($response, 200, [
// Cache this in the browser for an hour, and allow the browser to use a stale
// cache for up to a day after it was created while it fetches an updated set
// of translation keys.
'Cache-Control' => 'public, max-age=3600, stale-while-revalidate=86400',
'ETag' => md5(json_encode($response, JSON_THROW_ON_ERROR)),
]);
}
/**
* Convert standard Laravel translation keys that look like ":foo"
* into key structures that are supported by the front-end i18n
* library, like "{{foo}}".
*/
protected function i18n(array $data): array
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = $this->i18n($value);
} else {
// Find a Laravel style translation replacement in the string and replace it with
// one that the front-end is able to use. This won't always be present, especially
// for complex strings or things where we'd never have a backend component anyways.
//
// For example:
// "Hello :name, the :notifications.0.title notification needs :count actions :foo.0.bar."
//
// Becomes:
// "Hello {{name}}, the {{notifications.0.title}} notification needs {{count}} actions {{foo.0.bar}}."
$data[$key] = preg_replace('/:([\w.-]+\w)([^\w:]?|$)/m', '{{$1}}$2', $value);
}
}
return $data;
}
}

View File

@ -2,24 +2,17 @@
namespace App\Http\Middleware;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use Illuminate\Http\Request;
use Illuminate\Auth\AuthManager;
class RedirectIfAuthenticated
readonly class RedirectIfAuthenticated
{
/**
* RedirectIfAuthenticated constructor.
*/
public function __construct(private AuthManager $authManager) {}
/**
* Handle an incoming request.
*/
public function handle(Request $request, \Closure $next, ?string $guard = null): mixed
{
if ($this->authManager->guard($guard)->check()) {
return redirect(ListServers::getUrl());
return redirect('/');
}
return $next($request);

View File

@ -47,8 +47,7 @@ class PanelInstaller extends SimplePage implements HasForms
public static function isInstalled(): bool
{
// This defaults to true so existing panels count as "installed"
return env('APP_INSTALLED', true);
return config('app.installed');
}
public function mount(): void

View File

@ -14,7 +14,8 @@ class RequirementsStep
public static function make(): Step
{
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
$compare = version_compare(phpversion(), self::MIN_PHP_VERSION);
$correctPhpVersion = $compare >= 0;
$fields = [
Section::make('PHP Version')

View File

@ -85,12 +85,7 @@ class ActivityLog extends Model
public function actor(): MorphTo
{
$morph = $this->morphTo();
if (method_exists($morph, 'withTrashed')) {
return $morph->withTrashed();
}
return $morph;
return $this->morphTo()->withTrashed();
}
public function subjects(): HasMany

View File

@ -60,7 +60,7 @@ class AuditLog extends Model
*/
public static function instance(string $action, array $metadata, bool $isSystem = false): self
{
/** @var \Illuminate\Http\Request $request */
/** @var ?Request $request */
$request = Container::getInstance()->make('request');
if ($isSystem || !$request instanceof Request) {
$request = null;

View File

@ -2,7 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use App\Eloquent\BackupQueryBuilder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -81,10 +81,11 @@ class Backup extends Model
}
/**
* Returns a query filtering only non-failed backups for a specific server.
* @param \Illuminate\Database\Query\Builder $query
* @return BackupQueryBuilder<\Illuminate\Database\Eloquent\Model>
*/
public function scopeNonFailed(Builder $query): void
public function newEloquentBuilder($query): BackupQueryBuilder
{
$query->whereNull('completed_at')->orWhere('is_successful', true);
return new BackupQueryBuilder($query);
}
}

View File

@ -130,8 +130,7 @@ class File extends Model
public function getRows(): array
{
try {
/** @var DaemonFileRepository $fileRepository */
$fileRepository = app(DaemonFileRepository::class)->setServer(self::$server); // @phpstan-ignore-line
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));

View File

@ -307,8 +307,7 @@ class Node extends Model
{
return once(function () {
try {
// @phpstan-ignore-next-line
return resolve(DaemonConfigurationRepository::class)
return (new DaemonConfigurationRepository())
->setNode($this)
->getSystemInformation();
} catch (Exception $exception) {

View File

@ -342,6 +342,9 @@ class Server extends Model
return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');
}
/**
* @return HasMany<Backup, $this>
*/
public function backups(): HasMany
{
return $this->hasMany(Backup::class);
@ -487,7 +490,7 @@ class Server extends Model
public function condition(): Attribute
{
return Attribute::make(
get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status?->value ?? $this->retrieveStatus(),
get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status->value ?? $this->retrieveStatus(),
);
}

View File

@ -20,7 +20,6 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
use App\Models\Traits\HasAccessTokens;
use Illuminate\Auth\Passwords\CanResetPassword;
use App\Traits\Helpers\AvailableLanguages;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
@ -30,6 +29,7 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use ResourceBundle;
use Spatie\Permission\Traits\HasRoles;
/**
@ -89,7 +89,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{
use Authenticatable;
use Authorizable {can as protected canned; }
use AvailableLanguages;
use CanResetPassword;
use HasAccessTokens;
use HasRoles;
@ -179,9 +178,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
protected static function booted(): void
{
static::creating(function (self $user) {
$user->uuid = Str::uuid()->toString();
$user->timezone = env('APP_TIMEZONE', 'UTC');
$user->uuid ??= Str::uuid()->toString();
$user->timezone ??= config('app.timezone');
return true;
});
@ -206,8 +204,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{
$rules = parent::getRules();
$rules['language'][] = new In(array_keys((new self())->getAvailableLanguages()));
$rules['timezone'][] = new In(array_values(DateTimeZone::listIdentifiers()));
$rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang))));
$rules['timezone'][] = new In(DateTimeZone::listIdentifiers());
$rules['username'][] = new Username();
return $rules;
@ -257,6 +255,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
/**
* Returns all servers that a user owns.
*
* @return HasMany<Server, $this>
*/
public function servers(): HasMany
{

View File

@ -6,6 +6,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
class ForbiddenGlobalFunctionsRule implements Rule
{
@ -28,7 +29,10 @@ class ForbiddenGlobalFunctionsRule implements Rule
$functionName = (string) $node->name;
if (in_array($functionName, $this->forbiddenFunctions, true)) {
return [
sprintf('Usage of global function "%s" is forbidden.', $functionName),
RuleErrorBuilder::message(sprintf(
'Usage of global function "%s" is forbidden.',
$functionName,
))->identifier('myCustomRules.forbiddenGlobalFunctions')->build(),
];
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\Activity;
use Illuminate\Support\Arr;
use Throwable;
use Webmozart\Assert\Assert;
use Illuminate\Support\Collection;
use App\Models\ActivityLog;
@ -141,9 +142,8 @@ class ActivityLogService
try {
return $this->save();
} catch (\Throwable|\Exception $exception) {
} catch (Throwable $exception) {
if (config('app.env') !== 'production') {
/* @noinspection PhpUnhandledExceptionInspection */
throw $exception;
}
@ -216,9 +216,7 @@ class ActivityLogService
if ($actor = $this->targetable->actor()) {
$this->actor($actor);
} elseif ($user = $this->manager->guard()->user()) {
if ($user instanceof Model) {
$this->actor($user);
}
$this->actor($user);
}
return $this->activity;

View File

@ -14,7 +14,7 @@ use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class InitiateBackupService
{
private ?array $ignoredFiles;
private array $ignoredFiles;
private bool $isLocked = false;
@ -22,10 +22,10 @@ class InitiateBackupService
* InitiateBackupService constructor.
*/
public function __construct(
private ConnectionInterface $connection,
private DaemonBackupRepository $daemonBackupRepository,
private DeleteBackupService $deleteBackupService,
private BackupManager $backupManager
private readonly ConnectionInterface $connection,
private readonly DaemonBackupRepository $daemonBackupRepository,
private readonly DeleteBackupService $deleteBackupService,
private readonly BackupManager $backupManager
) {}
/**

View File

@ -8,18 +8,10 @@ use App\Models\Database;
use App\Models\DatabaseHost;
use App\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseService
readonly class DeployServerDatabaseService
{
/**
* DeployServerDatabaseService constructor.
*/
public function __construct(private DatabaseManagementService $managementService) {}
/**
* @throws \Throwable
* @throws \App\Exceptions\Service\Database\TooManyDatabasesException
* @throws \App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/
public function handle(Server $server, array $data): Database
{
Assert::notEmpty($data['database'] ?? null);

View File

@ -0,0 +1,40 @@
<?php
namespace App\Services\Helpers;
use Illuminate\Support\Facades\File;
use Locale;
class LanguageService
{
public const TRANSLATED_COMPLETELY = [
'ar',
'cz',
'da',
'de',
'dk',
'en',
'es',
'fi',
'ja',
'nl',
'pl',
'sk',
'ru',
'tr',
];
public function isLanguageTranslated(string $countryCode = 'en'): bool
{
return in_array($countryCode, self::TRANSLATED_COMPLETELY, true);
}
public function getAvailableLanguages(string $path = 'lang'): array
{
return collect(File::directories(base_path($path)))->mapWithKeys(function ($path) {
$code = basename($path);
return [$code => title_case(Locale::getDisplayName($code, $code))];
})->toArray();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\Nodes;
use Carbon\CarbonImmutable;
use DateTimeImmutable;
use Illuminate\Support\Str;
use App\Models\Node;
use App\Models\User;
@ -18,7 +19,7 @@ class NodeJWTService
private ?User $user = null;
private ?\DateTimeImmutable $expiresAt;
private DateTimeImmutable $expiresAt;
private ?string $subject = null;
@ -73,9 +74,7 @@ class NodeJWTService
->issuedAt(CarbonImmutable::now())
->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5));
if ($this->expiresAt) {
$builder = $builder->expiresAt($this->expiresAt);
}
$builder = $builder->expiresAt($this->expiresAt);
if (!empty($this->subject)) {
$builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject);

View File

@ -2,6 +2,7 @@
namespace App\Services\Schedules;
use App\Models\Task;
use Exception;
use App\Models\Schedule;
use Illuminate\Contracts\Bus\Dispatcher;
@ -12,18 +13,14 @@ use App\Repositories\Daemon\DaemonServerRepository;
class ProcessScheduleService
{
/**
* ProcessScheduleService constructor.
*/
public function __construct(private ConnectionInterface $connection, private Dispatcher $dispatcher, private DaemonServerRepository $serverRepository) {}
/**
* Process a schedule and push the first task onto the queue worker.
*
* @throws \Throwable
*/
public function handle(Schedule $schedule, bool $now = false): void
{
/** @var ?Task $task */
$task = $schedule->tasks()->orderBy('sequence_id')->first();
if (!$task) {

View File

@ -1,57 +0,0 @@
<?php
namespace App\Traits\Helpers;
use Locale;
use Illuminate\Filesystem\Filesystem;
trait AvailableLanguages
{
private ?Filesystem $filesystem = null;
public const TRANSLATED = [
'ar',
'cz',
'da',
'de',
'dk',
'en',
'es',
'fi',
'ja',
'nl',
'pl',
'sk',
'ru',
'tr',
];
/**
* Return all the available languages on the Panel based on those
* that are present in the language folder.
*/
public function getAvailableLanguages(): array
{
return collect($this->getFilesystemInstance()->directories(base_path('lang')))->mapWithKeys(function ($path) {
$code = basename($path);
$value = Locale::getDisplayName($code, $code);
return [$code => title_case($value)];
})->toArray();
}
public function isLanguageTranslated(string $countryCode = 'en'): bool
{
return in_array($countryCode, self::TRANSLATED, true);
}
/**
* Return an instance of the filesystem for getting a folder listing.
*/
private function getFilesystemInstance(): Filesystem
{
// @phpstan-ignore-next-line
return $this->filesystem = $this->filesystem ?: app()->make(Filesystem::class);
}
}

View File

@ -24,7 +24,7 @@ abstract class BaseTransformer extends TransformerAbstract
/**
* BaseTransformer constructor.
*/
public function __construct()
final public function __construct()
{
// Transformers allow for dependency injection on the handle method.
if (method_exists($this, 'handle')) {
@ -40,7 +40,7 @@ abstract class BaseTransformer extends TransformerAbstract
/**
* Sets the request on the instance.
*/
public function setRequest(Request $request): self
public function setRequest(Request $request): static
{
$this->request = $request;
@ -49,13 +49,10 @@ abstract class BaseTransformer extends TransformerAbstract
/**
* Returns a new transformer instance with the request set on the instance.
*
* @return static
*/
public static function fromRequest(Request $request): self
public static function fromRequest(Request $request): static
{
// @phpstan-ignore-next-line
return app(static::class)->setRequest($request);
return (new static())->setRequest($request);
}
/**

View File

@ -49,8 +49,7 @@ if (!function_exists('join_paths')) {
if (!function_exists('resolve_path')) {
function resolve_path(string $path): string
{
// @phpstan-ignore-next-line
$parts = array_filter(explode('/', $path), 'strlen');
$parts = array_filter(explode('/', $path), fn (string $p) => strlen($p) > 0);
$absolutes = [];
foreach ($parts as $part) {

View File

@ -49,7 +49,7 @@
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.0",
"fakerphp/faker": "^1.23.1",
"larastan/larastan": "^2.9.6",
"larastan/larastan": "^3.0",
"laravel/pint": "^1.15.3",
"laravel/sail": "^1.29.1",
"mockery/mockery": "^1.6.11",

702
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@ return [
'timezone' => 'UTC',
'installed' => env('APP_INSTALLED', true),
'exceptions' => [
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
],

View File

@ -12,6 +12,15 @@ parameters:
level: 6
ignoreErrors:
- '#no value type specified in iterable#'
- '#Unable to resolve the template type#'
- '#does not specify its types#'
- identifier: argument.templateType
- identifier: missingType.generics
- identifier: missingType.iterableValue
- identifier: property.notFound
# We are getting and setting environment variables directly
-
identifier: larastan.noEnvCallsOutsideOfConfig
paths:
- app/Console/Commands/Environment/*.php
- app/Extensions/OAuth/Providers/*.php
- app/Filament/Admin/Pages/Settings.php

View File

@ -3,5 +3,7 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth;
Route::redirect('/login', '/login')->name('auth.login');
Route::get('/oauth/redirect/{driver}', [Auth\OAuthController::class, 'redirect'])->name('auth.oauth.redirect');
Route::get('/oauth/callback/{driver}', [Auth\OAuthController::class, 'callback'])->name('auth.oauth.callback')->withoutMiddleware('guest');