Merge branch 'develop' into develop

This commit is contained in:
Dane Everitt 2020-10-31 13:47:12 -07:00 committed by GitHub
commit 665a4dd8a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
115 changed files with 3434 additions and 1970 deletions

View file

@ -144,6 +144,11 @@ class AppSettingsCommand extends Command
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(trans('command/messages.environment.app.settings'), true) ? 'false' : 'true';
}
// Make sure session cookies are set as "secure" when using HTTPS
if (strpos($this->variables['APP_URL'], 'https://') === 0) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->checkForRedis();
$this->writeToEnvironment($this->variables);

View file

@ -1,62 +1,37 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Console\Commands\Schedule;
use Cake\Chronos\Chronos;
use Throwable;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ProcessRunnableCommand extends Command
{
/**
* @var string
*/
protected $description = 'Process schedules in the database and determine which are ready to run.';
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
protected $processScheduleService;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
protected $signature = 'p:schedule:process';
/**
* @var string
*/
protected $signature = 'p:schedule:process';
/**
* ProcessRunnableCommand constructor.
*
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $processScheduleService
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(ProcessScheduleService $processScheduleService, ScheduleRepositoryInterface $repository)
{
parent::__construct();
$this->processScheduleService = $processScheduleService;
$this->repository = $repository;
}
protected $description = 'Process schedules in the database and determine which are ready to run.';
/**
* Handle command execution.
*/
public function handle()
{
$schedules = $this->repository->getSchedulesToProcess(Chronos::now()->toAtomString());
$schedules = Schedule::query()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->whereRaw('next_run_at <= NOW()')
->get();
if ($schedules->count() < 1) {
$this->line('There are no scheduled tasks for servers that need to be run.');
@ -64,23 +39,41 @@ class ProcessRunnableCommand extends Command
}
$bar = $this->output->createProgressBar(count($schedules));
$schedules->each(function ($schedule) use ($bar) {
if ($schedule->tasks instanceof Collection && count($schedule->tasks) > 0) {
$this->processScheduleService->handle($schedule);
if ($this->input->isInteractive()) {
$bar->clear();
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]));
}
}
foreach ($schedules as $schedule) {
$bar->clear();
$this->processSchedule($schedule);
$bar->advance();
$bar->display();
});
}
$this->line('');
}
/**
* Processes a given schedule and logs and errors encountered the console output. This should
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @see https://github.com/pterodactyl/panel/issues/2609
*/
protected function processSchedule(Schedule $schedule)
{
if ($schedule->tasks->isEmpty()) {
return;
}
try {
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
]));
} catch (Throwable | Exception $exception) {
Log::error($exception, ['schedule_id' => $schedule->id]);
$this->error("An error was encountered while processing Schedule #{$schedule->id}: " . $exception->getMessage());
}
}
}

View file

@ -58,8 +58,9 @@ class DeleteUserCommand extends Command
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
$results = User::query()
->where('email', 'LIKE', "$search%")
->where('username', 'LIKE', "$search%")
->where('id', 'LIKE', "$search%")
->orWhere('username', 'LIKE', "$search%")
->orWhere('email', 'LIKE', "$search%")
->get();
if (count($results) < 1) {

View file

@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
*/
public function findServerSchedules(int $server): Collection;
/**
* Load the tasks relationship onto the Schedule module if they are not
* already present.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @param bool $refresh
* @return \Pterodactyl\Models\Schedule
*/
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule;
/**
* Return a schedule model with all of the associated tasks as a relationship.
*
@ -34,12 +24,4 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getScheduleWithTasks(int $schedule): Schedule;
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection;
}

View file

@ -139,16 +139,4 @@ interface ServerRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator;
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node);
}

25
app/Helpers/Time.php Normal file
View file

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Helpers;
use Carbon\CarbonImmutable;
final class Time
{
/**
* Gets the time offset from the provided timezone relative to UTC as a number. This
* is used in the database configuration since we can't always rely on there being support
* for named timezones in MySQL.
*
* Returns the timezone as a string like +08:00 or -05:00 depending on the app timezone.
*
* @param string $timezone
* @return string
*/
public static function getMySQLTimezoneOffset(string $timezone): string
{
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600);
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad(abs($offset), 2, '0', STR_PAD_LEFT));
}
}

View file

@ -52,7 +52,12 @@ class Utilities
)->getNextRunDate());
}
public static function checked($name, $default)
/**
* @param string $name
* @param mixed $default
* @return string
*/
public static function checked(string $name, $default)
{
$errors = session('errors');

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Admin;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Mount;
@ -101,7 +102,6 @@ class MountController extends Controller
*/
public function create(MountFormRequest $request)
{
/** @var \Pterodactyl\Models\Mount $mount */
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);

View file

@ -6,7 +6,9 @@ use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\Contracts\View\Factory;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Models\Filters\AdminServerFilter;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
class ServerController extends Controller
@ -45,8 +47,10 @@ class ServerController extends Controller
public function index(Request $request)
{
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters(['uuid', 'name', 'image'])
->allowedSorts(['id', 'uuid'])
->allowedFilters([
AllowedFilter::exact('owner_id'),
AllowedFilter::custom('*', new AdminServerFilter),
])
->paginate(config()->get('pterodactyl.paginate.admin.servers'));
return $this->view->make('admin.servers.index', ['servers' => $servers]);

View file

@ -5,6 +5,8 @@ namespace Pterodactyl\Http\Controllers\Api\Client;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Permission;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Models\Filters\MultiFieldServerFilter;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
@ -43,21 +45,32 @@ class ClientController extends ClientApiController
// Start the query builder and ensure we eager load any requested relationships from the request.
$builder = QueryBuilder::for(
Server::query()->with($this->getIncludesForTransformer($transformer, ['node']))
)->allowedFilters('uuid', 'name', 'external_id');
)->allowedFilters([
'uuid',
'name',
'external_id',
AllowedFilter::custom('*', new MultiFieldServerFilter),
]);
$type = $request->input('type');
// Either return all of the servers the user has access to because they are an admin `?type=admin` or
// just return all of the servers the user has access to because they are the owner or a subuser of the
// server.
if ($request->input('type') === 'admin') {
$builder = $user->root_admin
? $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all())
// If they aren't an admin but want all the admin servers don't fail the request, just
// make it a query that will never return any results back.
: $builder->whereRaw('1 = 2');
} elseif ($request->input('type') === 'owner') {
$builder = $builder->where('owner_id', $user->id);
// server. If ?type=admin-all is passed all servers on the system will be returned to the user, rather
// than only servers they can see because they are an admin.
if (in_array($type, ['admin', 'admin-all'])) {
// If they aren't an admin but want all the admin servers don't fail the request, just
// make it a query that will never return any results back.
if (! $user->root_admin) {
$builder->whereRaw('1 = 2');
} else {
$builder = $type === 'admin-all'
? $builder
: $builder->whereNotIn('servers.id', $user->accessibleServers()->pluck('id')->all());
}
} else if ($type === 'owner') {
$builder = $builder->where('servers.owner_id', $user->id);
} else {
$builder = $builder->whereIn('id', $user->accessibleServers()->pluck('id')->all());
$builder = $builder->whereIn('servers.id', $user->accessibleServers()->pluck('id')->all());
}
$servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());

View file

@ -10,15 +10,19 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Models\Schedule;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Helpers\Utilities;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest;
class ScheduleController extends ClientApiController
{
@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController
*/
private $repository;
/**
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
*/
private $service;
/**
* ScheduleController constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $service
*/
public function __construct(ScheduleRepository $repository)
public function __construct(ScheduleRepository $repository, ProcessScheduleService $service)
{
parent::__construct();
$this->repository = $repository;
$this->service = $service;
}
/**
@ -147,6 +158,30 @@ class ScheduleController extends ClientApiController
->toArray();
}
/**
* Executes a given schedule immediately rather than waiting on it's normally scheduled time
* to pass. This does not care about the schedule state.
*
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Schedule $schedule
* @return \Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule)
{
if (!$schedule->is_active) {
throw new BadRequestHttpException(
'Cannot trigger schedule exection for a schedule that is not currently active.'
);
}
$this->service->handle($schedule, true);
return new JsonResponse([], JsonResponse::HTTP_ACCEPTED);
}
/**
* Deletes a schedule and it's associated tasks.
*

View file

@ -56,7 +56,8 @@ class ScheduleTaskController extends ClientApiController
);
}
$lastTask = $schedule->tasks->last();
/** @var \Pterodactyl\Models\Task|null $lastTask */
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
/** @var \Pterodactyl\Models\Task $task */
$task = $this->repository->create([
@ -102,13 +103,16 @@ class ScheduleTaskController extends ClientApiController
}
/**
* Determines if a user can delete the task for a given server.
* Delete a given task for a schedule. If there are subsequent tasks stored in the database
* for this schedule their sequence IDs are decremented properly.
*
* @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request
* @param \Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Schedule $schedule
* @param \Pterodactyl\Models\Task $task
* @return \Illuminate\Http\JsonResponse
*
* @throws \Exception
*/
public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task)
{
@ -120,8 +124,12 @@ class ScheduleTaskController extends ClientApiController
throw new HttpForbiddenException('You do not have permission to perform this action.');
}
$this->repository->delete($task->id);
$schedule->tasks()->where('sequence_id', '>', $task->sequence_id)->update([
'sequence_id' => $schedule->tasks()->getConnection()->raw('(sequence_id - 1)'),
]);
return JsonResponse::create(null, Response::HTTP_NO_CONTENT);
$task->delete();
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}
}

View file

@ -3,11 +3,14 @@
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerDetailsController extends Controller
@ -27,11 +30,6 @@ class ServerDetailsController extends Controller
*/
private $configurationStructureService;
/**
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
*/
private $nodeRepository;
/**
* ServerConfigurationController constructor.
*
@ -49,7 +47,6 @@ class ServerDetailsController extends Controller
$this->eggConfigurationService = $eggConfigurationService;
$this->repository = $repository;
$this->configurationStructureService = $configurationStructureService;
$this->nodeRepository = $nodeRepository;
}
/**
@ -66,7 +63,7 @@ class ServerDetailsController extends Controller
{
$server = $this->repository->getByUuid($uuid);
return JsonResponse::create([
return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server),
]);
@ -76,25 +73,19 @@ class ServerDetailsController extends Controller
* Lists all servers with their configurations that are assigned to the requesting node.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @return \Pterodactyl\Http\Resources\Wings\ServerConfigurationCollection
*/
public function list(Request $request)
{
/** @var \Pterodactyl\Models\Node $node */
$node = $request->attributes->get('node');
$servers = $this->repository->loadEveryServerForNode($node->id);
$configurations = [];
// Avoid run-away N+1 SQL queries by pre-loading the relationships that are used
// within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables', 'location')
->where('node_id', $node->id)
->paginate($request->input('per_page', 50));
foreach ($servers as $server) {
$configurations[$server->uuid] = [
'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server),
];
}
return JsonResponse::create($configurations);
return new ServerConfigurationCollection($servers);
}
}

View file

@ -2,11 +2,13 @@
namespace Pterodactyl\Http\Controllers\Auth;
use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -80,29 +82,31 @@ class LoginCheckpointController extends AbstractLoginController
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Exception
* @throws \Illuminate\Validation\ValidationException
*/
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
$token = $request->input('confirmation_token');
$recoveryToken = $request->input('recovery_token');
try {
/** @var \Pterodactyl\Models\User $user */
$user = $this->repository->find($this->cache->get($token, 0));
} catch (RecordNotFoundException $exception) {
return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.');
if ($this->hasTooManyLoginAttempts($request)) {
$this->sendLockoutResponse($request);
}
// If we got a recovery token try to find one that matches for the user and then continue
// through the process (and delete the token).
if (! is_null($recoveryToken)) {
foreach ($user->recoveryTokens as $token) {
if (password_verify($recoveryToken, $token->token)) {
$this->recoveryTokenRepository->delete($token->id);
$token = $request->input('confirmation_token');
try {
/** @var \Pterodactyl\Models\User $user */
$user = User::query()->findOrFail($this->cache->get($token, 0));
} catch (ModelNotFoundException $exception) {
$this->incrementLoginAttempts($request);
return $this->sendLoginResponse($user, $request);
}
return $this->sendFailedLoginResponse(
$request, null, 'The authentication token provided has expired, please refresh the page and try again.'
);
}
// Recovery tokens go through a slightly different pathway for usage.
if (! is_null($recoveryToken = $request->input('recovery_token'))) {
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
return $this->sendLoginResponse($user, $request);
}
} else {
$decrypted = $this->encrypter->decrypt($user->totp_secret);
@ -114,6 +118,31 @@ class LoginCheckpointController extends AbstractLoginController
}
}
$this->incrementLoginAttempts($request);
return $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.
*
* @param \Pterodactyl\Models\User $user
* @param string $value
* @return bool
*
* @throws \Exception
*/
protected function isValidRecoveryToken(User $user, string $value)
{
foreach ($user->recoveryTokens as $token) {
if (password_verify($value, $token->token)) {
$token->delete();
return true;
}
}
return false;
}
}

View file

@ -69,7 +69,7 @@ class DaemonAuthenticate
// Ensure that all of the correct parts are provided in the header.
if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
throw new BadRequestHttpException(
'The Authorization headed provided was not in a valid format.'
'The Authorization header provided was not in a valid format.'
);
}

View file

@ -55,6 +55,7 @@ class StoreServerRequest extends ApplicationApiRequest
'feature_limits' => 'required|array',
'feature_limits.databases' => $rules['database_limit'],
'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
// Placeholders for rules added in withValidator() function.
'allocation.default' => '',
@ -102,6 +103,7 @@ class StoreServerRequest extends ApplicationApiRequest
'start_on_completion' => array_get($data, 'start_on_completion', false),
'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
];
}

View file

@ -47,6 +47,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'feature_limits' => 'required|array',
'feature_limits.databases' => $rules['database_limit'],
'feature_limits.allocations' => $rules['allocation_limit'],
'feature_limits.backups' => $rules['backup_limit'],
];
}
@ -60,8 +61,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
$data = parent::validated();
$data['allocation_id'] = $data['allocation'];
$data['database_limit'] = $data['feature_limits']['databases'];
$data['allocation_limit'] = $data['feature_limits']['allocations'];
$data['database_limit'] = $data['feature_limits']['databases'] ?? null;
$data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null;
$data['backup_limit'] = $data['feature_limits']['backups'] ?? null;
unset($data['allocation'], $data['feature_limits']);
// Adjust the limits field to match what is expected by the model.
@ -90,6 +92,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'remove_allocations.*' => 'allocation to remove',
'feature_limits.databases' => 'Database Limit',
'feature_limits.allocations' => 'Allocation Limit',
'feature_limits.backups' => 'Backup Limit',
];
}

View file

@ -0,0 +1,25 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class TriggerScheduleRequest extends ClientApiRequest
{
/**
* @return string
*/
public function permission(): string
{
return Permission::ACTION_SCHEDULE_UPDATE;
}
/**
* @return array
*/
public function rules(): array
{
return [];
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Http\Resources\Wings;
use Pterodactyl\Models\Server;
use Illuminate\Container\Container;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Pterodactyl\Services\Eggs\EggConfigurationService;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
class ServerConfigurationCollection extends ResourceCollection
{
/**
* Converts a collection of Server models into an array of configuration responses
* that can be understood by Wings. Make sure you've properly loaded the required
* relationships on the Server models before calling this function, otherwise you'll
* have some serious performance issues from all of the N+1 queries.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$egg = Container::getInstance()->make(EggConfigurationService::class);
$configuration = Container::getInstance()->make(ServerConfigurationStructureService::class);
return $this->collection->map(function (Server $server) use ($configuration, $egg) {
return [
'uuid' => $server->uuid,
'settings' => $configuration->handle($server),
'process_configuration' => $egg->handle($server),
];
})->toArray();
}
}

View file

@ -7,11 +7,12 @@ use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task;
use InvalidArgumentException;
use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
@ -42,15 +43,13 @@ class RunTaskJob extends Job implements ShouldQueue
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
* @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
*
* @throws \Throwable
*/
public function handle(
DaemonCommandRepository $commandRepository,
InitiateBackupService $backupService,
DaemonPowerRepository $powerRepository,
TaskRepository $taskRepository
DaemonPowerRepository $powerRepository
) {
// Do not process a task that is not set to active.
if (! $this->task->schedule->is_active) {

View file

@ -0,0 +1,40 @@
<?php
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class AdminServerFilter implements Filter
{
/**
* A multi-column filter for the servers table that allows an administrative user to search
* across UUID, name, owner username, and owner email.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $value
* @param string $property
*/
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException(
'Cannot use the AdminServerFilter against a non-server model.'
);
}
$query
->select('servers.*')
->leftJoin('users', 'users.id', '=', 'servers.owner_id')
->where(function (Builder $builder) use ($value) {
$builder->where('servers.uuid', $value)
->orWhere('servers.uuid', 'LIKE', "$value%")
->orWhere('servers.uuidShort', $value)
->orWhere('servers.external_id', $value)
->orWhereRaw('LOWER(users.username) LIKE ?', ["%$value%"])
->orWhereRaw('LOWER(users.email) LIKE ?', ["$value%"])
->orWhereRaw('LOWER(servers.name) LIKE ?', ["%$value%"]);
})
->groupBy('servers.id');
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class MultiFieldServerFilter implements Filter
{
/**
* If we detect that the value matches an IPv4 address we will use a different type of filtering
* to look at the allocations.
*/
private const IPV4_REGEX = '/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(\:\d{1,5})?$/';
/**
* A multi-column filter for the servers table that allows you to pass in a single value and
* search across multiple columns. This allows us to provide a very generic search ability for
* the frontend.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $value
* @param string $property
*/
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException(
'Cannot use the MultiFieldServerFilter against a non-server model.'
);
}
if (preg_match(self::IPV4_REGEX, $value) || preg_match('/^:\d{1,5}$/', $value)) {
$query
// Only select the server values, otherwise you'll end up merging the allocation and
// server objects together, resulting in incorrect behavior and returned values.
->select('servers.*')
->join('allocations', 'allocations.server_id', '=', 'servers.id')
->where(function (Builder $builder) use ($value) {
$parts = explode(':', $value);
$builder->when(
!Str::startsWith($value, ':'),
// When the string does not start with a ":" it means we're looking for an IP or IP:Port
// combo, so use a query to handle that.
function (Builder $builder) use ($parts) {
$builder->orWhere('allocations.ip', $parts[0]);
if (!is_null($parts[1] ?? null)) {
$builder->where('allocations.port', 'LIKE', "{$parts[1]}%");
}
},
// Otherwise, just try to search for that specific port in the allocations.
function (Builder $builder) use ($value) {
$builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%');
}
);
})
->groupBy('servers.id');
return;
}
$query
->where(function (Builder $builder) use ($value) {
$builder->where('servers.uuid', $value)
->orWhere('servers.uuid', 'LIKE', "$value%")
->orWhere('servers.uuidShort', $value)
->orWhere('servers.external_id', $value)
->orWhereRaw('LOWER(servers.name) LIKE ?', ["%$value%"]);
});
}
}

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Models;
use Illuminate\Validation\Rules\NotIn;
/**
* @property int $id
* @property string $uuid
@ -63,6 +65,20 @@ class Mount extends Model
'user_mountable' => 'sometimes|boolean',
];
/**
* Implement language verification by overriding Eloquence's gather
* rules function.
*/
public static function getRules()
{
$rules = parent::getRules();
$rules['source'][] = new NotIn(Mount::$invalidSourcePaths);
$rules['target'][] = new NotIn(Mount::$invalidTargetPaths);
return $rules;
}
/**
* Disable timestamps on this model.
*
@ -70,6 +86,26 @@ class Mount extends Model
*/
public $timestamps = false;
/**
* Blacklisted source paths
*
* @var string[]
*/
public static $invalidSourcePaths = [
'/etc/pterodactyl',
'/var/lib/pterodactyl/volumes',
'/srv/daemon-data',
];
/**
* Blacklisted target paths
*
* @var string[]
*/
public static $invalidTargetPaths = [
'/home/container',
];
/**
* Returns all eggs that have this mount assigned.
*

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Models;
use Cron\CronExpression;
use Carbon\CarbonImmutable;
use Illuminate\Container\Container;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
@ -114,6 +116,20 @@ class Schedule extends Model
'next_run_at' => 'nullable|date',
];
/**
* Returns the schedule's execution crontab entry as a string.
*
* @return \Carbon\CarbonImmutable
*/
public function getNextRunDate()
{
$formatted = sprintf('%s %s %s * %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_day_of_week);
return CarbonImmutable::createFromTimestamp(
CronExpression::factory($formatted)->getNextRunDate()->getTimestamp()
);
}
/**
* Return a hashid encoded string to represent the ID of the schedule.
*

View file

@ -83,6 +83,13 @@ class Server extends Model
'oom_disabled' => true,
];
/**
* The default relationships to load for all server models.
*
* @var string[]
*/
protected $with = ['allocation'];
/**
* The attributes that should be mutated to dates.
*
@ -122,7 +129,7 @@ class Server extends Model
'installed' => 'in:0,1,2',
'database_limit' => 'present|nullable|integer|min:0',
'allocation_limit' => 'sometimes|nullable|integer|min:0',
'backup_limit' => 'present|integer|min:0',
'backup_limit' => 'present|nullable|integer|min:0',
];
/**

View file

@ -31,23 +31,6 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns());
}
/**
* Load the tasks relationship onto the Schedule module if they are not
* already present.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @param bool $refresh
* @return \Pterodactyl\Models\Schedule
*/
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule
{
if (! $schedule->relationLoaded('tasks') || $refresh) {
$schedule->load('tasks');
}
return $schedule;
}
/**
* Return a schedule model with all of the associated tasks as a relationship.
*
@ -64,19 +47,4 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor
throw new RecordNotFoundException;
}
}
/**
* Return all of the schedules that should be processed.
*
* @param string $timestamp
* @return \Illuminate\Support\Collection
*/
public function getSchedulesToProcess(string $timestamp): Collection
{
return $this->getBuilder()->with('tasks')
->where('is_active', true)
->where('is_processing', false)
->where('next_run_at', '<=', $timestamp)
->get($this->getColumns());
}
}

View file

@ -270,22 +270,4 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
->where('node_id', '=', $node)
->paginate($limit);
}
/**
* Returns every server that exists for a given node.
*
* This is different from {@see loadAllServersForNode} because
* it does not paginate the response.
*
* @param int $node
*
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function loadEveryServerForNode(int $node)
{
return $this->getBuilder()
->with('nest')
->where('node_id', '=', $node)
->get();
}
}

View file

@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Deployment;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Node;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService
@ -32,7 +31,7 @@ class FindViableNodesService
*/
public function setLocations(array $locations): self
{
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
Assert::allIntegerish($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations;
@ -97,8 +96,8 @@ class FindViableNodesService
}
$results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ])
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk])
->get()
->toBase();

View file

@ -2,12 +2,12 @@
namespace Pterodactyl\Services\Schedules;
use Cron\CronExpression;
use Exception;
use Pterodactyl\Models\Schedule;
use Illuminate\Contracts\Bus\Dispatcher;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
class ProcessScheduleService
{
@ -17,63 +17,66 @@ class ProcessScheduleService
private $dispatcher;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
* @var \Illuminate\Database\ConnectionInterface
*/
private $scheduleRepository;
/**
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
*/
private $taskRepository;
private $connection;
/**
* ProcessScheduleService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository
*/
public function __construct(
Dispatcher $dispatcher,
ScheduleRepositoryInterface $scheduleRepository,
TaskRepositoryInterface $taskRepository
) {
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
$this->scheduleRepository = $scheduleRepository;
$this->taskRepository = $taskRepository;
$this->connection = $connection;
}
/**
* Process a schedule and push the first task onto the queue worker.
*
* @param \Pterodactyl\Models\Schedule $schedule
* @param bool $now
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Throwable
*/
public function handle(Schedule $schedule)
public function handle(Schedule $schedule, bool $now = false)
{
$this->scheduleRepository->loadTasks($schedule);
/** @var \Pterodactyl\Models\Task $task */
$task = $schedule->getRelation('tasks')->where('sequence_id', 1)->first();
$task = $schedule->tasks()->orderBy('sequence_id', 'asc')->first();
$formattedCron = sprintf('%s %s %s * %s',
$schedule->cron_minute,
$schedule->cron_hour,
$schedule->cron_day_of_month,
$schedule->cron_day_of_week
);
if (is_null($task)) {
throw new DisplayException(
'Cannot process schedule for task execution: no tasks are registered.'
);
}
$this->scheduleRepository->update($schedule->id, [
'is_processing' => true,
'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(),
]);
$this->connection->transaction(function () use ($schedule, $task) {
$schedule->forceFill([
'is_processing' => true,
'next_run_at' => $schedule->getNextRunDate(),
])->saveOrFail();
$this->taskRepository->update($task->id, ['is_queued' => true]);
$task->update(['is_queued' => true]);
});
$this->dispatcher->dispatch(
(new RunTaskJob($task))->delay($task->time_offset)
);
$job = new RunTaskJob($task);
if (! $now) {
$this->dispatcher->dispatch($job->delay($task->time_offset));
} else {
// When using dispatchNow the RunTaskJob::failed() function is not called automatically
// so we need to manually trigger it and then continue with the exception throw.
//
// @see https://github.com/pterodactyl/panel/issues/2550
try {
$this->dispatcher->dispatchNow($job);
} catch (Exception $exception) {
$job->failed($exception);
throw $exception;
}
}
}
}

View file

@ -4,22 +4,14 @@ namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Server;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Models\Allocation;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class BuildModificationService
{
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
*/
private $allocationRepository;
/**
* @var \Illuminate\Database\ConnectionInterface
*/
@ -30,11 +22,6 @@ class BuildModificationService
*/
private $daemonServerRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
@ -43,23 +30,17 @@ class BuildModificationService
/**
* BuildModificationService constructor.
*
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(
AllocationRepositoryInterface $allocationRepository,
ServerConfigurationStructureService $structureService,
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
ServerRepositoryInterface $repository
DaemonServerRepository $daemonServerRepository
) {
$this->allocationRepository = $allocationRepository;
$this->daemonServerRepository = $daemonServerRepository;
$this->connection = $connection;
$this->repository = $repository;
$this->structureService = $structureService;
}
@ -70,9 +51,8 @@ class BuildModificationService
* @param array $data
* @return \Pterodactyl\Models\Server
*
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(Server $server, array $data)
{
@ -82,48 +62,35 @@ class BuildModificationService
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
try {
$this->allocationRepository->findFirstWhere([
['id', '=', $data['allocation_id']],
['server_id', '=', $server->id],
]);
} catch (RecordNotFoundException $ex) {
throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found'));
Allocation::query()->where('id', $data['allocation_id'])->where('server_id', $server->id)->firstOrFail();
} catch (ModelNotFoundException $ex) {
throw new DisplayException('The requested default allocation is not currently assigned to this server.');
}
}
/* @var \Pterodactyl\Models\Server $server */
$server = $this->repository->withFreshModel()->update($server->id, [
'oom_disabled' => array_get($data, 'oom_disabled'),
'memory' => array_get($data, 'memory'),
'swap' => array_get($data, 'swap'),
'io' => array_get($data, 'io'),
'cpu' => array_get($data, 'cpu'),
'threads' => array_get($data, 'threads'),
'disk' => array_get($data, 'disk'),
'allocation_id' => array_get($data, 'allocation_id'),
'database_limit' => array_get($data, 'database_limit', 0),
'allocation_limit' => array_get($data, 'allocation_limit', 0),
'backup_limit' => array_get($data, 'backup_limit', 0),
]);
// If any of these values are passed through in the data array go ahead and set
// them correctly on the server model.
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
$server->forceFill(array_merge($merge, [
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
'allocation_limit' => Arr::get($data, 'allocation_limit', 0) ?? null,
'backup_limit' => Arr::get($data, 'backup_limit', 0) ?? 0,
]))->saveOrFail();
$server = $server->fresh();
$updateData = $this->structureService->handle($server);
try {
$this->daemonServerRepository
->setServer($server)
->update(Arr::only($updateData, ['build']));
$this->daemonServerRepository->setServer($server)->update($updateData['build'] ?? []);
$this->connection->commit();
} catch (RequestException $exception) {
throw new DaemonConnectionException($exception);
}
$this->connection->commit();
return $server;
}
/**
* Process the allocations being assigned in the data and ensure they
* are available for a server.
* Process the allocations being assigned in the data and ensure they are available for a server.
*
* @param \Pterodactyl\Models\Server $server
* @param array $data
@ -132,55 +99,53 @@ class BuildModificationService
*/
private function processAllocations(Server $server, array &$data)
{
$firstAllocationId = null;
if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) {
if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
return;
}
// Handle the addition of allocations to this server.
if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) {
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id);
// Handle the addition of allocations to this server. Only assign allocations that are not currently
// assigned to a different server, and only allocations on the same node as the server.
if (! empty($data['add_allocations'])) {
$query = Allocation::query()
->where('node_id', $server->node_id)
->whereIn('id', $data['add_allocations'])
->whereNull('server_id');
$updateIds = [];
foreach ($data['add_allocations'] as $allocation) {
if (! in_array($allocation, $unassigned)) {
continue;
}
// Keep track of all the allocations we're just now adding so that we can use the first
// one to reset the default allocation to.
$freshlyAllocated = $query->pluck('id')->first();
$firstAllocationId = $firstAllocationId ?? $allocation;
$updateIds[] = $allocation;
}
if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
}
$query->update(['server_id' => $server->id, 'notes' => null]);
}
// Handle removal of allocations from this server.
if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) {
$assigned = $server->allocations->pluck('id')->toArray();
$updateIds = [];
if (! empty($data['remove_allocations'])) {
foreach ($data['remove_allocations'] as $allocation) {
if (! in_array($allocation, $assigned)) {
continue;
}
if ($allocation == $data['allocation_id']) {
if (is_null($firstAllocationId)) {
throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation'));
// If we are attempting to remove the default allocation for the server, see if we can reassign
// to the first provided value in add_allocations. If there is no new first allocation then we
// will throw an exception back.
if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) {
if (empty($freshlyAllocated)) {
throw new DisplayException(
'You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'
);
}
$data['allocation_id'] = $firstAllocationId;
// Update the default allocation to be the first allocation that we are creating.
$data['allocation_id'] = $freshlyAllocated;
}
$updateIds[] = $allocation;
}
if (! empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]);
}
// Remove any of the allocations we got that are currently assigned to this server on
// this node. Also set the notes to null, otherwise when re-allocated to a new server those
// notes will be carried over.
Allocation::query()->where('node_id', $server->node_id)
->where('server_id', $server->id)
// Only remove the allocations that we didn't also attempt to add to the server...
->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? []))
->update([
'notes' => null,
'server_id' => null,
]);
}
}
}

View file

@ -7,8 +7,6 @@ use Pterodactyl\Models\Server;
class ServerConfigurationStructureService
{
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'egg'];
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService
*/
@ -31,13 +29,11 @@ class ServerConfigurationStructureService
* daemon, if you modify the structure eggs will break unexpectedly.
*
* @param \Pterodactyl\Models\Server $server
* @param bool $legacy
* @param bool $legacy deprecated
* @return array
*/
public function handle(Server $server, bool $legacy = false): array
{
$server->loadMissing(self::REQUIRED_RELATIONS);
return $legacy ?
$this->returnLegacyFormat($server)
: $this->returnCurrentFormat($server);
@ -93,6 +89,7 @@ class ServerConfigurationStructureService
*
* @param \Pterodactyl\Models\Server $server
* @return array
* @deprecated
*/
protected function returnLegacyFormat(Server $server)
{