diff --git a/CHANGELOG.md b/CHANGELOG.md index efd2cef5b..faf955b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,49 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v1.0.0 +Pterodactyl 1.0 represents the culmination of over two years of work, almost 2,000 commits, endless bug and feature requests, and a dream that +has been in the making since 2013. 🎉 + +Due to the sheer size and timeline of this release I've massively truncated the listing below. There are numerous smaller +bug fixes and changes that would simply be too difficult to keep track of here. Please feel free to browse through the releases +tab for this repository to see more specific changes that have been made. + +### Added +* Adds a new client-facing API allowing a user to control all aspects of their individual servers, or servers +which they have been granted access to as a subuser. +* Adds the ability for backups to be created for a server both manually and via a scheduled task. +* Adds the ability for users to modify their server allocations on the fly and include notes for each allocation. +* Adds the ability for users to generate recovery tokens for 2FA protected logins which can be used in place of +a code should their device be inaccessible. +* Adds support for transfering servers between Nodes via the Panel. +* Adds the ability to assign specific CPU cores to a server (CPU Pinning) process. +* Server owners can now reinstall their assigned server egg automatically with a button on the frontend. + +### Changed +* The entire user frontend has been replaced with a responsive, React backed design implemented using Tailwind CSS. +* Replaces a large amount of complex daemon authentication logic by funneling most API calls through the Panel, and using +JSON Web Tokens where necessary to handle one-time direct authentication with Wings. +* Frontend server listing now includes a toggle to show or hide servers which an administrator has access to, rather +than always showing all servers on the system when logged into an admin account. +* We've replaced Ace Editor on the frontend with a better solution to allow lighter builds and more end-user functionality. +* Server permissions have been overhauled to be both easier to understand in the codebase, and allows plugins to better +hook into the permission system. + +### Removed +* Removes large swaths of code complexity and confusing interface designs that caused a lot of pain to new developers +trying to jump into the codebase. We've simplified this to stick to more established Laravel design standards to make +it easy to parse through the project and make contributions. + +## v0.7.19 (Derelict Dermodactylus) +### Fixed +* **[Security]** Fixes XSS in the admin area's server owner selection. + +## v0.7.18 (Derelict Dermodactylus) +### Fixed +* **[Security]** Re-addressed missed endpoint that would not properly limit a user account to 5 API keys. +* **[Security]** Addresses a Client API vulnerability that would allow a user to list all servers on the system ([`GHSA-6888-7f3w-92jx`](https://github.com/pterodactyl/panel/security/advisories/GHSA-6888-7f3w-92jx)) + ## v0.7.17 (Derelict Dermodactylus) ### Fixed * Limited accounts to 5 API keys at a time. @@ -301,7 +344,7 @@ the response from the server `GET` endpoint. * Nest and Egg listings now show the associated ID in order to make API requests easier. * Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. * Creating a new node will now requires a SSL connection if the Panel is configured to use SSL as well. -* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. +* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. * File manager now supports mass deletion option for files and folders. * Support for CS:GO as a default service option selection. * Support for GMOD as a default service option selection. @@ -431,7 +474,7 @@ the response from the server `GET` endpoint. * Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials. ### Added -* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. +* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/README.md b/README.md index a2d85a577..df1b21d4a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ [![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io) -[![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel) -[![Codecov](https://img.shields.io/codecov/c/github/pterodactyl/panel/develop.svg?style=flat-square)](https://codecov.io/gh/Pterodactyl/Panel) -[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord) +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge) +![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge) +![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge) +![GitHub Pre-Releases](https://img.shields.io/github/downloads-pre/pterodactyl/panel/v1.0.0-rc.7/total?style=for-the-badge) +![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge) # Pterodactyl Panel Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Designed with security diff --git a/app/Console/Commands/Overrides/SeedCommand.php b/app/Console/Commands/Overrides/SeedCommand.php new file mode 100644 index 000000000..91b555d4b --- /dev/null +++ b/app/Console/Commands/Overrides/SeedCommand.php @@ -0,0 +1,26 @@ +hasCompletedMigrations()) { + return $this->showMigrationWarning(); + } + + return parent::handle(); + } +} diff --git a/app/Console/Commands/Overrides/UpCommand.php b/app/Console/Commands/Overrides/UpCommand.php new file mode 100644 index 000000000..3a5f34610 --- /dev/null +++ b/app/Console/Commands/Overrides/UpCommand.php @@ -0,0 +1,23 @@ +hasCompletedMigrations()) { + return $this->showMigrationWarning(); + } + + return parent::handle(); + } +} diff --git a/app/Console/RequiresDatabaseMigrations.php b/app/Console/RequiresDatabaseMigrations.php new file mode 100644 index 000000000..f288ca1f1 --- /dev/null +++ b/app/Console/RequiresDatabaseMigrations.php @@ -0,0 +1,61 @@ +getLaravel()->make('migrator'); + + $files = $migrator->getMigrationFiles(database_path('migrations')); + + if (! $migrator->repositoryExists()) { + return false; + } + + if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) { + return false; + } + + return true; + } + + /** + * Throw a massive error into the console to hopefully catch the users attention and get + * them to properly run the migrations rather than ignoring all of the other previous + * errors... + * + * @return int + */ + protected function showMigrationWarning(): int + { + $this->getOutput()->writeln(" +| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | +| | +| Your database has not been properly migrated! | +| | +| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ | + +You must run the following command to finish migrating your database: + + php artisan migrate --step --force + +You will not be able to use Pterodactyl Panel as expected without fixing your +database state by running the command above. +"); + + $this->getOutput()->error("You must correct the error above before continuing."); + + return 1; + } +} diff --git a/app/Contracts/Repository/DatabaseRepositoryInterface.php b/app/Contracts/Repository/DatabaseRepositoryInterface.php index 967ca20fb..5926adb7c 100644 --- a/app/Contracts/Repository/DatabaseRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseRepositoryInterface.php @@ -42,18 +42,6 @@ interface DatabaseRepositoryInterface extends RepositoryInterface */ public function getDatabasesForHost(int $host, int $count = 25): LengthAwarePaginator; - /** - * Create a new database if it does not already exist on the host with - * the provided details. - * - * @param array $data - * @return \Pterodactyl\Models\Database - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException - */ - public function createIfNotExists(array $data): Database; - /** * Create a new database on a given connection. * diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 227989bab..76ed7da93 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -54,15 +54,4 @@ interface NodeRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getNodesForServerCreation(): Collection; - - /** - * Return the IDs of all nodes that exist in the provided locations and have the space - * available to support the additional disk and memory provided. - * - * @param array $locations - * @param int $disk - * @param int $memory - * @return \Illuminate\Support\LazyCollection - */ - public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection; } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f84c7077b..00484d943 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; class Handler extends ExceptionHandler { @@ -217,7 +218,9 @@ class Handler extends ExceptionHandler 'status' => method_exists($exception, 'getStatusCode') ? strval($exception->getStatusCode()) : ($exception instanceof ValidationException ? '422' : '500'), - 'detail' => 'An error was encountered while processing this request.', + 'detail' => $exception instanceof HttpExceptionInterface + ? $exception->getMessage() + : 'An unexpected error was encountered while processing this request, please try again.', ]; if ($exception instanceof ModelNotFoundException || $exception->getPrevious() instanceof ModelNotFoundException) { diff --git a/app/Http/Controllers/Admin/MountController.php b/app/Http/Controllers/Admin/MountController.php index 26cc6d369..3f40e555c 100644 --- a/app/Http/Controllers/Admin/MountController.php +++ b/app/Http/Controllers/Admin/MountController.php @@ -2,10 +2,12 @@ namespace Pterodactyl\Http\Controllers\Admin; +use Ramsey\Uuid\Uuid; use Illuminate\Http\Request; +use Pterodactyl\Models\Nest; use Pterodactyl\Models\Mount; +use Pterodactyl\Models\Location; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Mounts\MountUpdateService; use Pterodactyl\Http\Requests\Admin\MountFormRequest; @@ -37,21 +39,6 @@ class MountController extends Controller */ protected $repository; - /** - * @var \Pterodactyl\Services\Mounts\MountCreationService - */ - protected $creationService; - - /** - * @var \Pterodactyl\Services\Mounts\MountDeletionService - */ - protected $deletionService; - - /** - * @var \Pterodactyl\Services\Mounts\MountUpdateService - */ - protected $updateService; - /** * MountController constructor. * @@ -59,26 +46,17 @@ class MountController extends Controller * @param \Pterodactyl\Contracts\Repository\NestRepositoryInterface $nestRepository * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository - * @param \Pterodactyl\Services\Mounts\MountCreationService $creationService - * @param \Pterodactyl\Services\Mounts\MountDeletionService $deletionService - * @param \Pterodactyl\Services\Mounts\MountUpdateService $updateService */ public function __construct( AlertsMessageBag $alert, NestRepositoryInterface $nestRepository, LocationRepositoryInterface $locationRepository, - MountRepository $repository, - MountCreationService $creationService, - MountDeletionService $deletionService, - MountUpdateService $updateService + MountRepository $repository ) { $this->alert = $alert; $this->nestRepository = $nestRepository; $this->locationRepository = $locationRepository; $this->repository = $repository; - $this->creationService = $creationService; - $this->deletionService = $deletionService; - $this->updateService = $updateService; } /** @@ -103,11 +81,8 @@ class MountController extends Controller */ public function view($id) { - $nests = $this->nestRepository->all(); - $nests->load('eggs'); - - $locations = $this->locationRepository->all(); - $locations->load('nodes'); + $nests = Nest::query()->with('eggs')->get(); + $locations = Location::query()->with('nodes')->get(); return view('admin.mounts.view', [ 'mount' => $this->repository->getWithRelations($id), @@ -126,7 +101,13 @@ class MountController extends Controller */ public function create(MountFormRequest $request) { - $mount = $this->creationService->handle($request->normalize()); + /** @var \Pterodactyl\Models\Mount $mount */ + $model = (new Mount())->fill($request->validated()); + $model->forceFill(['uuid' => Uuid::uuid4()->toString()]); + + $model->saveOrFail(); + $mount = $model->fresh(); + $this->alert->success('Mount was created successfully.')->flash(); return redirect()->route('admin.mounts.view', $mount->id); @@ -147,7 +128,8 @@ class MountController extends Controller return $this->delete($mount); } - $this->updateService->handle($mount->id, $request->normalize()); + $mount->forceFill($request->validated())->save(); + $this->alert->success('Mount was updated successfully.')->flash(); return redirect()->route('admin.mounts.view', $mount->id); @@ -163,15 +145,9 @@ class MountController extends Controller */ public function delete(Mount $mount) { - try { - $this->deletionService->handle($mount->id); + $mount->delete(); - return redirect()->route('admin.mounts'); - } catch (DisplayException $ex) { - $this->alert->danger($ex->getMessage())->flash(); - } - - return redirect()->route('admin.mounts.view', $mount->id); + return redirect()->route('admin.mounts'); } /** @@ -188,11 +164,12 @@ class MountController extends Controller ]); $eggs = $validatedData['eggs'] ?? []; - if (sizeof($eggs) > 0) { - $mount->eggs()->attach(array_map('intval', $eggs)); - $this->alert->success('Mount was updated successfully.')->flash(); + if (count($eggs) > 0) { + $mount->eggs()->attach($eggs); } + $this->alert->success('Mount was updated successfully.')->flash(); + return redirect()->route('admin.mounts.view', $mount->id); } @@ -205,16 +182,15 @@ class MountController extends Controller */ public function addNodes(Request $request, Mount $mount) { - $validatedData = $request->validate([ - 'nodes' => 'required|exists:nodes,id', - ]); + $data = $request->validate(['nodes' => 'required|exists:nodes,id']); - $nodes = $validatedData['nodes'] ?? []; - if (sizeof($nodes) > 0) { - $mount->nodes()->attach(array_map('intval', $nodes)); - $this->alert->success('Mount was updated successfully.')->flash(); + $nodes = $data['nodes'] ?? []; + if (count($nodes) > 0) { + $mount->nodes()->attach($nodes); } + $this->alert->success('Mount was updated successfully.')->flash(); + return redirect()->route('admin.mounts.view', $mount->id); } diff --git a/app/Http/Controllers/Admin/Nests/EggScriptController.php b/app/Http/Controllers/Admin/Nests/EggScriptController.php index 298126a6f..ea8d4dfa9 100644 --- a/app/Http/Controllers/Admin/Nests/EggScriptController.php +++ b/app/Http/Controllers/Admin/Nests/EggScriptController.php @@ -1,15 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Admin\Nests; use Illuminate\View\View; +use Pterodactyl\Models\Egg; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; @@ -81,14 +75,14 @@ class EggScriptController extends Controller * Handle a request to update the installation script for an Egg. * * @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request - * @param int $egg + * @param \Pterodactyl\Models\Egg $egg * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException */ - public function update(EggScriptFormRequest $request, int $egg): RedirectResponse + public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse { $this->installScriptService->handle($egg, $request->normalize()); $this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash(); diff --git a/app/Http/Controllers/Admin/Nests/EggShareController.php b/app/Http/Controllers/Admin/Nests/EggShareController.php index 9b9403e4d..7845680e4 100644 --- a/app/Http/Controllers/Admin/Nests/EggShareController.php +++ b/app/Http/Controllers/Admin/Nests/EggShareController.php @@ -102,7 +102,7 @@ class EggShareController extends Controller * Update an existing Egg using a new imported file. * * @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request - * @param int $egg + * @param \Pterodactyl\Models\Egg $egg * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -110,7 +110,7 @@ class EggShareController extends Controller * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException */ - public function update(EggImportFormRequest $request, int $egg): RedirectResponse + public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse { $this->updateImporterService->handle($egg, $request->file('import_file')); $this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash(); diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 94df5dc21..324e7c3b0 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -12,7 +12,9 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Support\Arr; use Illuminate\Http\Request; use Pterodactyl\Models\User; +use Pterodactyl\Models\Mount; use Pterodactyl\Models\Server; +use Pterodactyl\Models\MountServer; use Prologue\Alerts\AlertsMessageBag; use GuzzleHttp\Exception\RequestException; use Pterodactyl\Exceptions\DisplayException; @@ -251,7 +253,7 @@ class ServersController extends Controller */ public function reinstallServer(Server $server) { - $this->reinstallService->reinstall($server); + $this->reinstallService->handle($server); $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); return redirect()->route('admin.servers.view.manage', $server->id); @@ -332,13 +334,18 @@ class ServersController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Illuminate\Validation\ValidationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function saveStartup(Request $request, Server $server) { - $this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN); - $this->startupModificationService->handle($server, $request->except('_token')); + try { + $this->startupModificationService + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($server, $request->except('_token')); + } catch (DataValidationException $exception) { + throw new ValidationException($exception->validator); + } + $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); return redirect()->route('admin.servers.view.startup', $server->id); @@ -356,7 +363,7 @@ class ServersController extends Controller public function newDatabase(StoreServerDatabaseRequest $request, Server $server) { $this->databaseManagementService->create($server, [ - 'database' => $request->input('database'), + 'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id), 'remote' => $request->input('remote'), 'database_host_id' => $request->input('database_host_id'), 'max_connections' => $request->input('max_connections'), @@ -403,7 +410,7 @@ class ServersController extends Controller ['id', '=', $database], ]); - $this->databaseManagementService->delete($database->id); + $this->databaseManagementService->delete($database); return response('', 204); } @@ -412,12 +419,17 @@ class ServersController extends Controller * Add a mount to a server. * * @param Server $server - * @param int $mount_id + * @param \Pterodactyl\Models\Mount $mount + * * @return \Illuminate\Http\RedirectResponse + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException|\Throwable */ - public function addMount(Server $server, int $mount_id) + public function addMount(Server $server, Mount $mount) { - $server->mounts()->attach($mount_id); + $mountServer = new MountServer; + $mountServer->mount_id = $mount->id; + $mountServer->server_id = $server->id; + $mountServer->saveOrFail(); $data = $this->serverConfigurationStructureService->handle($server); @@ -438,15 +450,15 @@ class ServersController extends Controller * Remove a mount from a server. * * @param Server $server - * @param int $mount_id + * @param \Pterodactyl\Models\Mount $mount * @return \Illuminate\Http\RedirectResponse * - * @throws DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function deleteMount(Server $server, int $mount_id) + public function deleteMount(Server $server, Mount $mount) { - $server->mounts()->detach($mount_id); + MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete(); $data = $this->serverConfigurationStructureService->handle($server); diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index e0b53c343..b5126c766 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -86,8 +86,8 @@ class UserController extends Controller { $users = QueryBuilder::for( User::query()->select('users.*') - ->selectRaw('COUNT(subusers.id) as subuser_of_count') - ->selectRaw('COUNT(servers.id) as servers_count') + ->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count') + ->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count') ->leftJoin('subusers', 'subusers.user_id', '=', 'users.id') ->leftJoin('servers', 'servers.owner_id', '=', 'users.id') ->groupBy('users.id') diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index 24c8906aa..829a6ca5d 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -110,7 +110,9 @@ class DatabaseController extends ApplicationApiController */ public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse { - $database = $this->databaseManagementService->create($server, $request->validated()); + $database = $this->databaseManagementService->create($server, array_merge($request->validated(), [ + 'database' => $request->databaseName(), + ])); return $this->fractal->item($database) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) @@ -133,7 +135,7 @@ class DatabaseController extends ApplicationApiController */ public function delete(ServerDatabaseWriteRequest $request): Response { - $this->databaseManagementService->delete($request->getModel(Database::class)->id); + $this->databaseManagementService->delete($request->getModel(Database::class)); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php index 90372f220..5052c884e 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -82,7 +82,7 @@ class ServerManagementController extends ApplicationApiController */ public function reinstall(ServerWriteRequest $request, Server $server): Response { - $this->reinstallServerService->reinstall($server); + $this->reinstallServerService->handle($server); return $this->returnNoContent(); } diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 5a3e1e3ac..2eacfb9e2 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -129,7 +129,7 @@ class DatabaseController extends ClientApiController */ public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response { - $this->managementService->delete($database->id); + $this->managementService->delete($database); return Response::create('', Response::HTTP_NO_CONTENT); } diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index ed3c97b34..a175f390f 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -70,7 +70,7 @@ class FileController extends ClientApiController { $contents = $this->fileRepository ->setServer($server) - ->getDirectory(urlencode($request->get('directory') ?? '/')); + ->getDirectory(urlencode(urldecode($request->get('directory') ?? '/'))); return $this->fractal->collection($contents) ->transformWith($this->getTransformer(FileObjectTransformer::class)) @@ -91,7 +91,7 @@ class FileController extends ClientApiController { return new Response( $this->fileRepository->setServer($server)->getContent( - urlencode($request->get('file')), config('pterodactyl.files.max_edit_size') + urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size') ), Response::HTTP_OK, ['Content-Type' => 'text/plain'] diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index 593bbc259..d35b597ed 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -120,15 +120,27 @@ class ScheduleController extends ClientApiController */ public function update(UpdateScheduleRequest $request, Server $server, Schedule $schedule) { - $this->repository->update($schedule->id, [ + $active = (bool) $request->input('is_active'); + + $data = [ 'name' => $request->input('name'), 'cron_day_of_week' => $request->input('day_of_week'), 'cron_day_of_month' => $request->input('day_of_month'), 'cron_hour' => $request->input('hour'), 'cron_minute' => $request->input('minute'), - 'is_active' => (bool) $request->input('is_active'), + 'is_active' => $active, 'next_run_at' => $this->getNextRunAt($request), - ]); + ]; + + // Toggle the processing state of the scheduled task when it is enabled or disabled so that an + // invalid state can be reset without manual database intervention. + // + // @see https://github.com/pterodactyl/panel/issues/2425 + if ($schedule->is_active !== $active) { + $data['is_processing'] = false; + } + + $this->repository->update($schedule->id, $data); return $this->fractal->item($schedule->refresh()) ->transformWith($this->getTransformer(ScheduleTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/Servers/SettingsController.php b/app/Http/Controllers/Api/Client/Servers/SettingsController.php index 6090906ee..7dfbf7b4f 100644 --- a/app/Http/Controllers/Api/Client/Servers/SettingsController.php +++ b/app/Http/Controllers/Api/Client/Servers/SettingsController.php @@ -69,7 +69,7 @@ class SettingsController extends ClientApiController */ public function reinstall(ReinstallServerRequest $request, Server $server) { - $this->reinstallServerService->reinstall($server); + $this->reinstallServerService->handle($server); return new JsonResponse([], Response::HTTP_ACCEPTED); } diff --git a/app/Http/Controllers/Api/Client/Servers/StartupController.php b/app/Http/Controllers/Api/Client/Servers/StartupController.php index 8ab62a02c..e0c580279 100644 --- a/app/Http/Controllers/Api/Client/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Client/Servers/StartupController.php @@ -100,7 +100,7 @@ class StartupController extends ClientApiController 'server_id' => $server->id, 'variable_id' => $variable->id, ], [ - 'variable_value' => $request->input('value'), + 'variable_value' => $request->input('value') ?? '', ]); $variable = $variable->refresh(); diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php index 77879c97f..7ab597b63 100644 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -49,11 +49,11 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings return Database::query()->where('id', $id)->firstOrFail(); }); - $this->router->model('backup', Backup::class, function ($value) { + $this->router->bind('backup', function ($value) { return Backup::query()->where('uuid', $value)->firstOrFail(); }); - $this->router->model('user', User::class, function ($value) { + $this->router->bind('user', function ($value) { return User::query()->where('uuid', $value)->firstOrFail(); }); diff --git a/app/Http/Requests/Admin/Egg/EggFormRequest.php b/app/Http/Requests/Admin/Egg/EggFormRequest.php index 6fe577cac..bda0e8c4d 100644 --- a/app/Http/Requests/Admin/Egg/EggFormRequest.php +++ b/app/Http/Requests/Admin/Egg/EggFormRequest.php @@ -19,12 +19,12 @@ class EggFormRequest extends AdminFormRequest public function rules() { $rules = [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:191', 'description' => 'nullable|string', - 'docker_image' => 'required|string|max:255', + 'docker_image' => 'required|string|max:191', 'startup' => 'required|string', 'config_from' => 'sometimes|bail|nullable|numeric', - 'config_stop' => 'required_without:config_from|nullable|string|max:255', + 'config_stop' => 'required_without:config_from|nullable|string|max:191', 'config_startup' => 'required_without:config_from|nullable|json', 'config_logs' => 'required_without:config_from|nullable|json', 'config_files' => 'required_without:config_from|nullable|json', diff --git a/app/Http/Requests/Admin/Egg/EggVariableFormRequest.php b/app/Http/Requests/Admin/Egg/EggVariableFormRequest.php index 933bf8348..d52fe94d2 100644 --- a/app/Http/Requests/Admin/Egg/EggVariableFormRequest.php +++ b/app/Http/Requests/Admin/Egg/EggVariableFormRequest.php @@ -15,9 +15,9 @@ class EggVariableFormRequest extends AdminFormRequest public function rules() { return [ - 'name' => 'required|string|min:1|max:255', + 'name' => 'required|string|min:1|max:191', 'description' => 'sometimes|nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES, + 'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES, 'options' => 'sometimes|required|array', 'rules' => 'bail|required|string', 'default_value' => 'present', diff --git a/app/Http/Requests/Admin/MountFormRequest.php b/app/Http/Requests/Admin/MountFormRequest.php index b6647b16b..bd94a633a 100644 --- a/app/Http/Requests/Admin/MountFormRequest.php +++ b/app/Http/Requests/Admin/MountFormRequest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Requests\Admin; diff --git a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php index 333723578..2f01dfe9e 100644 --- a/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php +++ b/app/Http/Requests/Admin/Nest/StoreNestFormRequest.php @@ -19,7 +19,7 @@ class StoreNestFormRequest extends AdminFormRequest public function rules() { return [ - 'name' => 'required|string|min:1|max:255', + 'name' => 'required|string|min:1|max:191', 'description' => 'string|nullable', ]; } diff --git a/app/Http/Requests/Admin/Node/AllocationFormRequest.php b/app/Http/Requests/Admin/Node/AllocationFormRequest.php index 777d3033f..3c580c026 100644 --- a/app/Http/Requests/Admin/Node/AllocationFormRequest.php +++ b/app/Http/Requests/Admin/Node/AllocationFormRequest.php @@ -20,7 +20,7 @@ class AllocationFormRequest extends AdminFormRequest { return [ 'allocation_ip' => 'required|string', - 'allocation_alias' => 'sometimes|nullable|string|max:255', + 'allocation_alias' => 'sometimes|nullable|string|max:191', 'allocation_ports' => 'required|array', ]; } diff --git a/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php index 023b2d89a..496268293 100644 --- a/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/AdvancedSettingsFormRequest.php @@ -15,8 +15,8 @@ class AdvancedSettingsFormRequest extends AdminFormRequest { return [ 'recaptcha:enabled' => 'required|in:true,false', - 'recaptcha:secret_key' => 'required|string|max:255', - 'recaptcha:website_key' => 'required|string|max:255', + 'recaptcha:secret_key' => 'required|string|max:191', + 'recaptcha:website_key' => 'required|string|max:191', 'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60', 'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60', 'pterodactyl:console:count' => 'required|integer|min:1', diff --git a/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php index 777761b67..208c15b10 100644 --- a/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/BaseSettingsFormRequest.php @@ -16,7 +16,7 @@ class BaseSettingsFormRequest extends AdminFormRequest public function rules() { return [ - 'app:name' => 'required|string|max:255', + 'app:name' => 'required|string|max:191', 'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2', 'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))], 'app:analytics' => 'nullable|string', diff --git a/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php index 92a23272f..728283af4 100644 --- a/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php @@ -18,10 +18,10 @@ class MailSettingsFormRequest extends AdminFormRequest 'mail:host' => 'required|string', 'mail:port' => 'required|integer|between:1,65535', 'mail:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])], - 'mail:username' => 'nullable|string|max:255', - 'mail:password' => 'nullable|string|max:255', + 'mail:username' => 'nullable|string|max:191', + 'mail:password' => 'nullable|string|max:191', 'mail:from:address' => 'required|string|email', - 'mail:from:name' => 'nullable|string|max:255', + 'mail:from:name' => 'nullable|string|max:191', ]; } diff --git a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php index f795a114e..1e68b82e5 100644 --- a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php @@ -24,7 +24,7 @@ class StoreAllocationRequest extends ApplicationApiRequest { return [ 'ip' => 'required|string', - 'alias' => 'sometimes|nullable|string|max:255', + 'alias' => 'sometimes|nullable|string|max:191', 'ports' => 'required|array', 'ports.*' => 'string', ]; diff --git a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php index c2dbfe14a..4ca019410 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php @@ -2,9 +2,12 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; +use Webmozart\Assert\Assert; +use Pterodactyl\Models\Server; use Illuminate\Validation\Rule; use Illuminate\Database\Query\Builder; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; class StoreServerDatabaseRequest extends ApplicationApiRequest @@ -26,14 +29,16 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest */ public function rules(): array { + $server = $this->route()->parameter('server'); + return [ 'database' => [ 'required', - 'string', + 'alpha_dash', 'min:1', - 'max:24', - Rule::unique('databases')->where(function (Builder $query) { - $query->where('database_host_id', $this->input('host') ?? 0); + 'max:48', + Rule::unique('databases')->where(function (Builder $query) use ($server) { + $query->where('server_id', $server->id)->where('database', $this->databaseName()); }), ], 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', @@ -68,4 +73,18 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest 'database' => 'Database Name', ]; } + + /** + * Returns the database name in the expected format. + * + * @return string + */ + public function databaseName(): string + { + $server = $this->route()->parameter('server'); + + Assert::isInstanceOf($server, Server::class); + + return DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id); + } } diff --git a/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php b/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php index a82db1ec0..1a2632862 100644 --- a/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php +++ b/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Api\Client\Account; +use Pterodactyl\Models\ApiKey; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; class StoreApiKeyRequest extends ClientApiRequest @@ -11,9 +12,11 @@ class StoreApiKeyRequest extends ClientApiRequest */ public function rules(): array { + $rules = ApiKey::getRules(); + return [ - 'description' => 'required|string|min:4', - 'allowed_ips' => 'array', + 'description' => $rules['memo'], + 'allowed_ips' => $rules['allowed_ips'], 'allowed_ips.*' => 'ip', ]; } diff --git a/app/Http/Requests/Api/Client/Servers/Backups/StoreBackupRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/StoreBackupRequest.php index e6dd2ad43..4ca892155 100644 --- a/app/Http/Requests/Api/Client/Servers/Backups/StoreBackupRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Backups/StoreBackupRequest.php @@ -21,7 +21,7 @@ class StoreBackupRequest extends ClientApiRequest public function rules(): array { return [ - 'name' => 'nullable|string|max:255', + 'name' => 'nullable|string|max:191', 'ignored' => 'nullable|string', ]; } diff --git a/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php index 3f2fe29eb..42bc8587c 100644 --- a/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php @@ -2,9 +2,14 @@ namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases; +use Webmozart\Assert\Assert; +use Pterodactyl\Models\Server; +use Illuminate\Validation\Rule; use Pterodactyl\Models\Permission; +use Illuminate\Database\Query\Builder; use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; +use Pterodactyl\Services\Databases\DatabaseManagementService; class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest { @@ -21,9 +26,35 @@ class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissions */ public function rules(): array { + $server = $this->route()->parameter('server'); + + Assert::isInstanceOf($server, Server::class); + return [ - 'database' => 'required|alpha_dash|min:3|max:48', + 'database' => [ + 'required', + 'alpha_dash', + 'min:1', + 'max:48', + // Yes, I am aware that you could have the same database name across two unique hosts. However, + // I don't really care about that for this validation. We just want to make sure it is unique to + // the server itself. No need for complexity. + Rule::unique('databases')->where(function (Builder $query) use ($server) { + $query->where('server_id', $server->id) + ->where('database', DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id)); + }), + ], 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', ]; } + + /** + * @return array + */ + public function messages() + { + return [ + 'database.unique' => 'The database name you have selected is already in use by this server.', + ]; + } } diff --git a/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php b/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php index 63005c78b..b46e6ea9a 100644 --- a/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php @@ -24,7 +24,7 @@ class UpdateStartupVariableRequest extends ClientApiRequest { return [ 'key' => 'required|string', - 'value' => 'present|string', + 'value' => 'present', ]; } } diff --git a/app/Http/Requests/Api/Client/Servers/Subusers/StoreSubuserRequest.php b/app/Http/Requests/Api/Client/Servers/Subusers/StoreSubuserRequest.php index effc3bf3a..0848770f9 100644 --- a/app/Http/Requests/Api/Client/Servers/Subusers/StoreSubuserRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Subusers/StoreSubuserRequest.php @@ -20,7 +20,7 @@ class StoreSubuserRequest extends SubuserRequest public function rules(): array { return [ - 'email' => 'required|email', + 'email' => 'required|email|between:1,191', 'permissions' => 'required|array', 'permissions.*' => 'string', ]; diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index 4f6bfd22f..bab1d61db 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -3,10 +3,10 @@ namespace Pterodactyl\Jobs\Schedule; use Exception; -use Carbon\Carbon; use Pterodactyl\Jobs\Job; +use Carbon\CarbonImmutable; +use Pterodactyl\Models\Task; use InvalidArgumentException; -use Illuminate\Container\Container; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; @@ -15,39 +15,25 @@ use Pterodactyl\Repositories\Eloquent\TaskRepository; use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository; -use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; -use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; class RunTaskJob extends Job implements ShouldQueue { use DispatchesJobs, InteractsWithQueue, SerializesModels; /** - * @var int - */ - public $schedule; - - /** - * @var int + * @var \Pterodactyl\Models\Task */ public $task; - /** - * @var \Pterodactyl\Repositories\Eloquent\TaskRepository - */ - protected $taskRepository; - /** * RunTaskJob constructor. * - * @param int $task - * @param int $schedule + * @param \Pterodactyl\Models\Task $task */ - public function __construct(int $task, int $schedule) + public function __construct(Task $task) { $this->queue = config('pterodactyl.queues.standard'); $this->task = $task; - $this->schedule = $schedule; } /** @@ -58,7 +44,6 @@ class RunTaskJob extends Job implements ShouldQueue * @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository * @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Throwable */ public function handle( @@ -67,36 +52,32 @@ class RunTaskJob extends Job implements ShouldQueue DaemonPowerRepository $powerRepository, TaskRepository $taskRepository ) { - $this->taskRepository = $taskRepository; - - $task = $this->taskRepository->getTaskForJobProcess($this->task); - $server = $task->getRelation('server'); - // Do not process a task that is not set to active. - if (! $task->getRelation('schedule')->is_active) { + if (! $this->task->schedule->is_active) { $this->markTaskNotQueued(); $this->markScheduleComplete(); return; } + $server = $this->task->server; // Perform the provided task against the daemon. - switch ($task->action) { + switch ($this->task->action) { case 'power': - $powerRepository->setServer($server)->send($task->payload); + $powerRepository->setServer($server)->send($this->task->payload); break; case 'command': - $commandRepository->setServer($server)->send($task->payload); + $commandRepository->setServer($server)->send($this->task->payload); break; case 'backup': - $backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null); + $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null); break; default: throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); } $this->markTaskNotQueued(); - $this->queueNextTask($task->sequence_id); + $this->queueNextTask(); } /** @@ -112,23 +93,23 @@ class RunTaskJob extends Job implements ShouldQueue /** * Get the next task in the schedule and queue it for running after the defined period of wait time. - * - * @param int $sequence - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - private function queueNextTask($sequence) + private function queueNextTask() { - $nextTask = $this->taskRepository->getNextTask($this->schedule, $sequence); + /** @var \Pterodactyl\Models\Task|null $nextTask */ + $nextTask = Task::query()->where('schedule_id', $this->task->schedule_id) + ->where('sequence_id', $this->task->sequence_id + 1) + ->first(); + if (is_null($nextTask)) { $this->markScheduleComplete(); return; } - $this->taskRepository->update($nextTask->id, ['is_queued' => true]); - $this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset)); + $nextTask->update(['is_queued' => true]); + + $this->dispatch((new self($nextTask))->delay($nextTask->time_offset)); } /** @@ -136,13 +117,10 @@ class RunTaskJob extends Job implements ShouldQueue */ private function markScheduleComplete() { - Container::getInstance() - ->make(ScheduleRepositoryInterface::class) - ->withoutFreshModel() - ->update($this->schedule, [ - 'is_processing' => false, - 'last_run_at' => Carbon::now()->toDateTimeString(), - ]); + $this->task->schedule()->update([ + 'is_processing' => false, + 'last_run_at' => CarbonImmutable::now()->toDateTimeString(), + ]); } /** @@ -150,8 +128,6 @@ class RunTaskJob extends Job implements ShouldQueue */ private function markTaskNotQueued() { - Container::getInstance() - ->make(TaskRepositoryInterface::class) - ->update($this->task, ['is_queued' => false]); + $this->task->update(['is_queued' => false]); } } diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 016702141..750ca0de9 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -50,7 +50,7 @@ class DatabaseHost extends Model * @var array */ public static $validationRules = [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:191', 'host' => 'required|string', 'port' => 'required|numeric|between:1,65535', 'username' => 'required|string|max:32', diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 143fe95a8..4aa33beff 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -93,13 +93,13 @@ class Egg extends Model public static $validationRules = [ 'nest_id' => 'required|bail|numeric|exists:nests,id', 'uuid' => 'required|string|size:36', - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:191', 'description' => 'string|nullable', 'author' => 'required|string|email', - 'docker_image' => 'required|string|max:255', + 'docker_image' => 'required|string|max:191', 'startup' => 'required|nullable|string', 'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id', - 'config_stop' => 'required_without:config_from|nullable|string|max:255', + 'config_stop' => 'required_without:config_from|nullable|string|max:191', 'config_startup' => 'required_without:config_from|nullable|json', 'config_logs' => 'required_without:config_from|nullable|json', 'config_files' => 'required_without:config_from|nullable|json', diff --git a/app/Models/EggMount.php b/app/Models/EggMount.php new file mode 100644 index 000000000..cd85673ce --- /dev/null +++ b/app/Models/EggMount.php @@ -0,0 +1,21 @@ + 'exists:eggs,id', - 'name' => 'required|string|between:1,255', + 'name' => 'required|string|between:1,191', 'description' => 'string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . self::RESERVED_ENV_NAMES, + 'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . self::RESERVED_ENV_NAMES, 'default_value' => 'string', 'user_viewable' => 'boolean', 'user_editable' => 'boolean', diff --git a/app/Models/Location.php b/app/Models/Location.php index 17ba7e24a..74fed1812 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -41,7 +41,7 @@ class Location extends Model */ public static $validationRules = [ 'short' => 'required|string|between:1,60|unique:locations,short', - 'long' => 'string|nullable|between:1,255', + 'long' => 'string|nullable|between:1,191', ]; /** diff --git a/app/Models/Model.php b/app/Models/Model.php index 095fe7adc..f6b94a3ab 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -7,6 +7,7 @@ use Illuminate\Validation\Rule; use Illuminate\Container\Container; use Illuminate\Contracts\Validation\Factory; use Illuminate\Database\Eloquent\Model as IlluminateModel; +use Pterodactyl\Exceptions\Model\DataValidationException; abstract class Model extends IlluminateModel { @@ -55,7 +56,11 @@ abstract class Model extends IlluminateModel static::$validatorFactory = Container::getInstance()->make(Factory::class); static::saving(function (Model $model) { - return $model->validate(); + if (! $model->validate()) { + throw new DataValidationException($model->getValidator()); + } + + return true; }); } @@ -147,9 +152,9 @@ abstract class Model extends IlluminateModel } return $this->getValidator()->setData( - // Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist - // for that model. Doing this will return all of the attributes in a format that can - // properly be validated. + // Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist + // for that model. Doing this will return all of the attributes in a format that can + // properly be validated. $this->addCastAttributesToArray( $this->getAttributes(), $this->getMutatedAttributes() ) diff --git a/app/Models/Mount.php b/app/Models/Mount.php index 023243f93..b69c0c78d 100644 --- a/app/Models/Mount.php +++ b/app/Models/Mount.php @@ -56,7 +56,7 @@ class Mount extends Model */ public static $validationRules = [ 'name' => 'required|string|min:2|max:64|unique:mounts,name', - 'description' => 'nullable|string|max:255', + 'description' => 'nullable|string|max:191', 'source' => 'required|string', 'target' => 'required|string', 'read_only' => 'sometimes|boolean', diff --git a/app/Models/MountServer.php b/app/Models/MountServer.php index 3999b0c8e..21bf8fe2d 100644 --- a/app/Models/MountServer.php +++ b/app/Models/MountServer.php @@ -11,6 +11,11 @@ class MountServer extends Model */ protected $table = 'mount_server'; + /** + * @var bool + */ + public $timestamps = false; + /** * @var null */ diff --git a/app/Models/Nest.php b/app/Models/Nest.php index 5def1c833..770c5baf7 100644 --- a/app/Models/Nest.php +++ b/app/Models/Nest.php @@ -44,7 +44,7 @@ class Nest extends Model */ public static $validationRules = [ 'author' => 'required|string|email', - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:191', 'description' => 'nullable|string', ]; diff --git a/app/Models/Permission.php b/app/Models/Permission.php index f870866e2..96429f31c 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -219,80 +219,4 @@ class Permission extends Model { return Collection::make(self::$permissions); } - - /** - * A list of all permissions available for a user. - * - * @var array - * @deprecated - */ - protected static $deprecatedPermissions = [ - 'power' => [ - 'power-start' => 's:power:start', - 'power-stop' => 's:power:stop', - 'power-restart' => 's:power:restart', - 'power-kill' => 's:power:kill', - 'send-command' => 's:command', - ], - 'subuser' => [ - 'list-subusers' => null, - 'view-subuser' => null, - 'edit-subuser' => null, - 'create-subuser' => null, - 'delete-subuser' => null, - ], - 'server' => [ - 'view-allocations' => null, - 'edit-allocation' => null, - 'view-startup' => null, - 'edit-startup' => null, - ], - 'database' => [ - 'view-databases' => null, - 'reset-db-password' => null, - 'delete-database' => null, - 'create-database' => null, - ], - 'file' => [ - 'access-sftp' => null, - 'list-files' => 's:files:get', - 'edit-files' => 's:files:read', - 'save-files' => 's:files:post', - 'move-files' => 's:files:move', - 'copy-files' => 's:files:copy', - 'compress-files' => 's:files:compress', - 'decompress-files' => 's:files:decompress', - 'create-files' => 's:files:create', - 'upload-files' => 's:files:upload', - 'delete-files' => 's:files:delete', - 'download-files' => 's:files:download', - ], - 'task' => [ - 'list-schedules' => null, - 'view-schedule' => null, - 'toggle-schedule' => null, - 'queue-schedule' => null, - 'edit-schedule' => null, - 'create-schedule' => null, - 'delete-schedule' => null, - ], - ]; - - /** - * Return a collection of permissions available. - * - * @param bool $array - * @return array|\Illuminate\Database\Eloquent\Collection - * @deprecated - */ - public static function getPermissions($array = false) - { - if ($array) { - return collect(self::$deprecatedPermissions)->mapWithKeys(function ($item) { - return $item; - })->all(); - } - - return collect(self::$deprecatedPermissions); - } } diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index bc214f27d..d737edd2c 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -103,7 +103,7 @@ class Schedule extends Model */ public static $validationRules = [ 'server_id' => 'required|exists:servers,id', - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:191', 'cron_day_of_week' => 'required|string', 'cron_day_of_month' => 'required|string', 'cron_hour' => 'required|string', diff --git a/app/Models/Server.php b/app/Models/Server.php index f6de3516c..aa4a39f06 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -15,7 +15,7 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property string $name * @property string $description * @property bool $skip_scripts - * @property int $suspended + * @property bool $suspended * @property int $owner_id * @property int $memory * @property int $swap @@ -103,7 +103,7 @@ class Server extends Model public static $validationRules = [ 'external_id' => 'sometimes|nullable|string|between:1,191|unique:servers', 'owner_id' => 'required|integer|exists:users,id', - 'name' => 'required|string|min:1|max:255', + 'name' => 'required|string|min:1|max:191', 'node_id' => 'required|exists:nodes,id', 'description' => 'string', 'memory' => 'required|numeric|min:0', @@ -118,7 +118,7 @@ class Server extends Model 'egg_id' => 'required|exists:eggs,id', 'startup' => 'required|string', 'skip_scripts' => 'sometimes|boolean', - 'image' => 'required|string|max:255', + 'image' => 'required|string|max:191', 'installed' => 'in:0,1,2', 'database_limit' => 'present|nullable|integer|min:0', 'allocation_limit' => 'sometimes|nullable|integer|min:0', @@ -133,7 +133,7 @@ class Server extends Model protected $casts = [ 'node_id' => 'integer', 'skip_scripts' => 'boolean', - 'suspended' => 'integer', + 'suspended' => 'boolean', 'owner_id' => 'integer', 'memory' => 'integer', 'swap' => 'integer', diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 1a91a578e..458248628 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -25,7 +25,7 @@ class Setting extends Model * @var array */ public static $validationRules = [ - 'key' => 'required|string|between:1,255', + 'key' => 'required|string|between:1,191', 'value' => 'string', ]; } diff --git a/app/Models/User.php b/app/Models/User.php index d2761bd72..24ef981f4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -137,11 +137,11 @@ class User extends Model implements */ public static $validationRules = [ 'uuid' => 'required|string|size:36|unique:users,uuid', - 'email' => 'required|email|unique:users,email', - 'external_id' => 'sometimes|nullable|string|max:255|unique:users,external_id', - 'username' => 'required|between:1,255|unique:users,username', - 'name_first' => 'required|string|between:1,255', - 'name_last' => 'required|string|between:1,255', + 'email' => 'required|email|between:1,191|unique:users,email', + 'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id', + 'username' => 'required|between:1,191|unique:users,username', + 'name_first' => 'required|string|between:1,191', + 'name_last' => 'required|string|between:1,191', 'password' => 'sometimes|nullable|string', 'root_admin' => 'boolean', 'language' => 'string', diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index 48dec217b..46b3916da 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -93,31 +93,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor ->paginate($count, $this->getColumns()); } - /** - * Create a new database if it does not already exist on the host with - * the provided details. - * - * @param array $data - * @return \Pterodactyl\Models\Database - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException - */ - public function createIfNotExists(array $data): Database - { - $count = $this->getBuilder()->where([ - ['server_id', '=', array_get($data, 'server_id')], - ['database_host_id', '=', array_get($data, 'database_host_id')], - ['database', '=', array_get($data, 'database')], - ])->count(); - - if ($count > 0) { - throw new DuplicateDatabaseNameException('A database with those details already exists for the specified server.'); - } - - return $this->create($data); - } - /** * Create a new database on a given connection. * diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index b7463001e..9c852fb56 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -171,28 +171,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa return $instance->first(); } - - /** - * Return the IDs of all nodes that exist in the provided locations and have the space - * available to support the additional disk and memory provided. - * - * @param array $locations - * @param int $disk - * @param int $memory - * @return \Illuminate\Support\LazyCollection - */ - public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection - { - $instance = $this->getBuilder() - ->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate']) - ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk') - ->leftJoin('servers', 'servers.node_id', '=', 'nodes.id') - ->where('nodes.public', 1); - - if (! empty($locations)) { - $instance->whereIn('nodes.location_id', $locations); - } - - return $instance->groupBy('nodes.id')->cursor(); - } } diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index ab415b1ab..832e4bdf1 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -3,18 +3,29 @@ namespace Pterodactyl\Services\Databases; use Exception; +use InvalidArgumentException; use Pterodactyl\Models\Server; use Pterodactyl\Models\Database; use Pterodactyl\Helpers\Utilities; use Illuminate\Database\ConnectionInterface; +use Symfony\Component\VarDumper\Cloner\Data; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\DatabaseRepository; +use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException; use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException; class DatabaseManagementService { + /** + * The regex used to validate that the database name passed through to the function is + * in the expected format. + * + * @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName() + */ + private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/'; + /** * @var \Illuminate\Database\ConnectionInterface */ @@ -31,7 +42,7 @@ class DatabaseManagementService private $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository */ private $repository; @@ -50,13 +61,13 @@ class DatabaseManagementService * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic - * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( ConnectionInterface $connection, DynamicDatabaseConnection $dynamic, - DatabaseRepositoryInterface $repository, + DatabaseRepository $repository, Encrypter $encrypter ) { $this->connection = $connection; @@ -65,6 +76,21 @@ class DatabaseManagementService $this->repository = $repository; } + /** + * Generates a unique database name for the given server. This name should be passed through when + * calling this handle function for this service, otherwise the database will be created with + * whatever name is provided. + * + * @param string $name + * @param int $serverId + * @return string + */ + public static function generateUniqueDatabaseName(string $name, int $serverId): string + { + // Max of 48 characters, including the s123_ that we append to the front. + return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_"))); + } + /** * Set wether or not this class should validate that the server has enough slots * left before creating the new database. @@ -104,12 +130,15 @@ class DatabaseManagementService } } - // Max of 48 characters, including the s123_ that we append to the front. - $truncatedName = substr($data['database'], 0, 48 - strlen("s{$server->id}_")); + // Protect against developer mistakes... + if (empty($data['database']) || ! preg_match(self::MATCH_NAME_REGEX, $data['database'])) { + throw new InvalidArgumentException( + 'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".' + ); + } $data = array_merge($data, [ 'server_id' => $server->id, - 'database' => $truncatedName, 'username' => sprintf('u%d_%s', $server->id, str_random(10)), 'password' => $this->encrypter->encrypt( Utilities::randomStringWithSpecialCharacters(24) @@ -120,7 +149,8 @@ class DatabaseManagementService try { return $this->connection->transaction(function () use ($data, &$database) { - $database = $this->repository->createIfNotExists($data); + $database = $this->createModel($data); + $this->dynamic->set('dynamic', $data['database_host_id']); $this->repository->createDatabase($database->database); @@ -139,7 +169,7 @@ class DatabaseManagementService $this->repository->dropUser($database->username, $database->remote); $this->repository->flush(); } - } catch (Exception $exception) { + } catch (Exception $deletionException) { // Do nothing here. We've already encountered an issue before this point so no // reason to prioritize this error over the initial one. } @@ -151,20 +181,48 @@ class DatabaseManagementService /** * Delete a database from the given host server. * - * @param int $id + * @param \Pterodactyl\Models\Database $database * @return bool|null * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Exception */ - public function delete($id) + public function delete(Database $database) { - $database = $this->repository->find($id); $this->dynamic->set('dynamic', $database->database_host_id); $this->repository->dropDatabase($database->database); $this->repository->dropUser($database->username, $database->remote); $this->repository->flush(); - return $this->repository->delete($id); + return $database->delete(); + } + + /** + * Create the database if there is not an identical match in the DB. While you can technically + * have the same name across multiple hosts, for the sake of keeping this logic easy to understand + * and avoiding user confusion we will ignore the specific host and just look across all hosts. + * + * @param array $data + * @return \Pterodactyl\Models\Database + * + * @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException + * @throws \Throwable + */ + protected function createModel(array $data): Database + { + $exists = Database::query()->where('server_id', $data['server_id']) + ->where('database', $data['database']) + ->exists(); + + if ($exists) { + throw new DuplicateDatabaseNameException( + 'A database with that name already exists for this server.' + ); + } + + $database = (new Database)->forceFill($data); + $database->saveOrFail(); + + return $database; } } diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index 734740324..4bc72a1fd 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -2,44 +2,27 @@ namespace Pterodactyl\Services\Databases; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Pterodactyl\Models\Database; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Models\DatabaseHost; use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException; class DeployServerDatabaseService { - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface - */ - private $databaseHostRepository; - /** * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ private $managementService; - /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface - */ - private $repository; - /** * ServerDatabaseCreationService constructor. * - * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository - * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService */ - public function __construct( - DatabaseRepositoryInterface $repository, - DatabaseHostRepositoryInterface $databaseHostRepository, - DatabaseManagementService $managementService - ) { - $this->databaseHostRepository = $databaseHostRepository; + public function __construct(DatabaseManagementService $managementService) + { $this->managementService = $managementService; - $this->repository = $repository; } /** @@ -53,28 +36,26 @@ class DeployServerDatabaseService */ public function handle(Server $server, array $data): Database { - $allowRandom = config('pterodactyl.client_features.databases.allow_random'); - $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ - ['node_id', '=', $server->node_id], - ]); - - if ($hosts->isEmpty() && ! $allowRandom) { - throw new NoSuitableDatabaseHostException; - } + Assert::notEmpty($data['database'] ?? null); + Assert::notEmpty($data['remote'] ?? null); + $hosts = DatabaseHost::query()->get()->toBase(); if ($hosts->isEmpty()) { - $hosts = $this->databaseHostRepository->setColumns(['id'])->all(); - if ($hosts->isEmpty()) { + throw new NoSuitableDatabaseHostException; + } else { + $nodeHosts = $hosts->where('node_id', $server->node_id)->toBase(); + + if ($nodeHosts->isEmpty() && ! config('pterodactyl.client_features.databases.allow_random')) { throw new NoSuitableDatabaseHostException; } } - $host = $hosts->random(); - return $this->managementService->create($server, [ - 'database_host_id' => $host->id, - 'database' => array_get($data, 'database'), - 'remote' => array_get($data, 'remote'), + 'database_host_id' => $nodeHosts->isEmpty() + ? $hosts->random()->id + : $nodeHosts->random()->id, + 'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id), + 'remote' => $data['remote'], ]); } } diff --git a/app/Services/Deployment/FindViableNodesService.php b/app/Services/Deployment/FindViableNodesService.php index 6d6832c27..d89c73f5e 100644 --- a/app/Services/Deployment/FindViableNodesService.php +++ b/app/Services/Deployment/FindViableNodesService.php @@ -3,16 +3,12 @@ namespace Pterodactyl\Services\Deployment; use Webmozart\Assert\Assert; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Models\Node; +use Illuminate\Support\LazyCollection; use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException; class FindViableNodesService { - /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface - */ - private $repository; - /** * @var array */ @@ -28,16 +24,6 @@ class FindViableNodesService */ protected $memory; - /** - * FindViableNodesService constructor. - * - * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository - */ - public function __construct(NodeRepositoryInterface $repository) - { - $this->repository = $repository; - } - /** * Set the locations that should be searched through to locate available nodes. * @@ -46,6 +32,8 @@ class FindViableNodesService */ public function setLocations(array $locations): self { + Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.'); + $this->locations = $locations; return $this; @@ -90,32 +78,34 @@ class FindViableNodesService * are tossed out, as are any nodes marked as non-public, meaning automatic * deployments should not be done against them. * - * @return int[] + * @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException */ - public function handle(): array + public function handle() { - Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s'); - Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s'); + Assert::integer($this->disk, 'Disk space must be an int, got %s'); + Assert::integer($this->memory, 'Memory usage must be an int, got %s'); - $nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory); - $viable = []; + $query = Node::query()->select('nodes.*') + ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory') + ->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk') + ->leftJoin('servers', 'servers.node_id', '=', 'nodes.id') + ->where('nodes.public', 1); - foreach ($nodes as $node) { - $memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100)); - $diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100)); - - if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) { - continue; - } - - $viable[] = $node->id; + if (! empty($this->locations)) { + $query = $query->whereIn('nodes.location_id', $this->locations); } - if (empty($viable)) { + $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 ]) + ->get() + ->toBase(); + + if ($results->isEmpty()) { throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes')); } - return $viable; + return $results; } } diff --git a/app/Services/Eggs/Scripts/InstallScriptService.php b/app/Services/Eggs/Scripts/InstallScriptService.php index d51447568..70621c32b 100644 --- a/app/Services/Eggs/Scripts/InstallScriptService.php +++ b/app/Services/Eggs/Scripts/InstallScriptService.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Eggs\Scripts; @@ -40,12 +33,8 @@ class InstallScriptService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException */ - public function handle($egg, array $data) + public function handle(Egg $egg, array $data) { - if (! $egg instanceof Egg) { - $egg = $this->repository->find($egg); - } - if (! is_null(array_get($data, 'copy_script_from'))) { if (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) { throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id')); diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index 25a7131fa..1602fc919 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Eggs\Sharing; diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 006360cfa..96c4c72eb 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Eggs\Sharing; diff --git a/app/Services/Eggs/Sharing/EggUpdateImporterService.php b/app/Services/Eggs/Sharing/EggUpdateImporterService.php index 3acf3f90e..91b8d2b2e 100644 --- a/app/Services/Eggs/Sharing/EggUpdateImporterService.php +++ b/app/Services/Eggs/Sharing/EggUpdateImporterService.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Services\Eggs\Sharing; +use Pterodactyl\Models\Egg; use Illuminate\Http\UploadedFile; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Contracts\Repository\EggRepositoryInterface; @@ -46,7 +47,7 @@ class EggUpdateImporterService /** * Update an existing Egg using an uploaded JSON file. * - * @param int $egg + * @param \Pterodactyl\Models\Egg $egg * @param \Illuminate\Http\UploadedFile $file * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -54,7 +55,7 @@ class EggUpdateImporterService * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException */ - public function handle(int $egg, UploadedFile $file) + public function handle(Egg $egg, UploadedFile $file) { if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) { throw new InvalidFileUploadException( @@ -81,7 +82,7 @@ class EggUpdateImporterService } $this->connection->beginTransaction(); - $this->repository->update($egg, [ + $this->repository->update($egg->id, [ 'author' => object_get($parsed, 'author'), 'name' => object_get($parsed, 'name'), 'description' => object_get($parsed, 'description'), @@ -99,19 +100,19 @@ class EggUpdateImporterService // Update Existing Variables collect($parsed->variables)->each(function ($variable) use ($egg) { $this->variableRepository->withoutFreshModel()->updateOrCreate([ - 'egg_id' => $egg, + 'egg_id' => $egg->id, 'env_variable' => $variable->env_variable, ], collect($variable)->except(['egg_id', 'env_variable'])->toArray()); }); $imported = collect($parsed->variables)->pluck('env_variable')->toArray(); - $existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg]]); + $existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg->id]]); // Delete variables not present in the import. collect($existing)->each(function ($variable) use ($egg, $imported) { if (! in_array($variable->env_variable, $imported)) { $this->variableRepository->deleteWhere([ - ['egg_id', '=', $egg], + ['egg_id', '=', $egg->id], ['env_variable', '=', $variable->env_variable], ]); } diff --git a/app/Services/Mounts/MountCreationService.php b/app/Services/Mounts/MountCreationService.php deleted file mode 100644 index 923ba9abb..000000000 --- a/app/Services/Mounts/MountCreationService.php +++ /dev/null @@ -1,40 +0,0 @@ -repository = $repository; - } - - /** - * Create a new mount. - * - * @param array $data - * @return \Pterodactyl\Models\Mount - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function handle(array $data) - { - return $this->repository->create(array_merge($data, [ - 'uuid' => Uuid::uuid4()->toString(), - ]), true, true); - } -} diff --git a/app/Services/Mounts/MountDeletionService.php b/app/Services/Mounts/MountDeletionService.php deleted file mode 100644 index 0850e685b..000000000 --- a/app/Services/Mounts/MountDeletionService.php +++ /dev/null @@ -1,40 +0,0 @@ -repository = $repository; - } - - /** - * Delete an existing location. - * - * @param int|\Pterodactyl\Models\Mount $mount - * @return int|null - */ - public function handle($mount) - { - $mount = ($mount instanceof Mount) ? $mount->id : $mount; - - Assert::integerish($mount, 'First argument passed to handle must be numeric or an instance of ' . Mount::class . ', received %s.'); - - return $this->repository->delete($mount); - } -} diff --git a/app/Services/Mounts/MountUpdateService.php b/app/Services/Mounts/MountUpdateService.php deleted file mode 100644 index a66f1ea91..000000000 --- a/app/Services/Mounts/MountUpdateService.php +++ /dev/null @@ -1,41 +0,0 @@ -repository = $repository; - } - - /** - * Update an existing location. - * - * @param int|\Pterodactyl\Models\Mount $mount - * @param array $data - * @return \Pterodactyl\Models\Mount - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle($mount, array $data) - { - $mount = ($mount instanceof Mount) ? $mount->id : $mount; - - return $this->repository->update($mount, $data); - } -} diff --git a/app/Services/Schedules/ProcessScheduleService.php b/app/Services/Schedules/ProcessScheduleService.php index ca27d1878..3fa3604a4 100644 --- a/app/Services/Schedules/ProcessScheduleService.php +++ b/app/Services/Schedules/ProcessScheduleService.php @@ -73,7 +73,7 @@ class ProcessScheduleService $this->taskRepository->update($task->id, ['is_queued' => true]); $this->dispatcher->dispatch( - (new RunTaskJob($task->id, $schedule->id))->delay($task->time_offset) + (new RunTaskJob($task))->delay($task->time_offset) ); } } diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 8aab214d1..6f7b590f7 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -4,8 +4,6 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; use Pterodactyl\Models\EggVariable; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentService { @@ -14,28 +12,6 @@ class EnvironmentService */ private $additional = []; - /** - * @var \Illuminate\Contracts\Config\Repository - */ - private $config; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - private $repository; - - /** - * EnvironmentService constructor. - * - * @param \Illuminate\Contracts\Config\Repository $config - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - */ - public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository) - { - $this->config = $config; - $this->repository = $repository; - } - /** * Dynamically configure additional environment variables to be assigned * with a specific server. @@ -79,7 +55,7 @@ class EnvironmentService } // Process variables set in the configuration file. - foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) { + foreach (config('pterodactyl.environment_variables', []) as $key => $object) { $variables->put( $key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object) ); diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index aff0321ce..6f5b56083 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -19,26 +19,18 @@ class ReinstallServerService */ private $connection; - /** - * @var \Pterodactyl\Repositories\Eloquent\ServerRepository - */ - private $repository; - /** * ReinstallService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository - * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository */ public function __construct( ConnectionInterface $connection, - DaemonServerRepository $daemonServerRepository, - ServerRepository $repository + DaemonServerRepository $daemonServerRepository ) { $this->daemonServerRepository = $daemonServerRepository; $this->connection = $connection; - $this->repository = $repository; } /** @@ -49,16 +41,14 @@ class ReinstallServerService * * @throws \Throwable */ - public function reinstall(Server $server) + public function handle(Server $server) { return $this->connection->transaction(function () use ($server) { - $updated = $this->repository->update($server->id, [ - 'installed' => Server::STATUS_INSTALLING, - ], true, true); + $server->forceFill(['installed' => Server::STATUS_INSTALLING])->save(); $this->daemonServerRepository->setServer($server)->reinstall(); - return $updated; + return $server->refresh(); }); } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index f17405fad..fb4412170 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -1,17 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Mount; use Pterodactyl\Models\Server; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class ServerConfigurationStructureService { @@ -22,22 +14,13 @@ class ServerConfigurationStructureService */ private $environment; - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - private $repository; - /** * ServerConfigurationStructureService constructor. * - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Services\Servers\EnvironmentService $environment */ - public function __construct( - ServerRepositoryInterface $repository, - EnvironmentService $environment - ) { - $this->repository = $repository; + public function __construct(EnvironmentService $environment) + { $this->environment = $environment; } @@ -50,8 +33,6 @@ class ServerConfigurationStructureService * @param \Pterodactyl\Models\Server $server * @param bool $legacy * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle(Server $server, bool $legacy = false): array { @@ -72,7 +53,7 @@ class ServerConfigurationStructureService { return [ 'uuid' => $server->uuid, - 'suspended' => (bool) $server->suspended, + 'suspended' => $server->suspended, 'environment' => $this->environment->handle($server), 'invocation' => $server->startup, 'skip_egg_scripts' => $server->skip_scripts, @@ -112,8 +93,6 @@ class ServerConfigurationStructureService * * @param \Pterodactyl\Models\Server $server * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ protected function returnLegacyFormat(Server $server) { diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 589a790e9..3a1b45733 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -4,7 +4,9 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\User; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; use Pterodactyl\Models\Allocation; @@ -13,7 +15,6 @@ use Pterodactyl\Models\Objects\DeploymentObject; use Pterodactyl\Repositories\Eloquent\EggRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository; -use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Services\Deployment\FindViableNodesService; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Services\Deployment\AllocationSelectionService; @@ -21,11 +22,6 @@ use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; class ServerCreationService { - /** - * @var \Pterodactyl\Repositories\Eloquent\AllocationRepository - */ - private $allocationRepository; - /** * @var \Pterodactyl\Services\Deployment\AllocationSelectionService */ @@ -79,7 +75,6 @@ class ServerCreationService /** * CreationService constructor. * - * @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $allocationRepository * @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository @@ -92,7 +87,6 @@ class ServerCreationService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ public function __construct( - AllocationRepository $allocationRepository, AllocationSelectionService $allocationSelectionService, ConnectionInterface $connection, DaemonServerRepository $daemonServerRepository, @@ -105,7 +99,6 @@ class ServerCreationService VariableValidatorService $validatorService ) { $this->allocationSelectionService = $allocationSelectionService; - $this->allocationRepository = $allocationRepository; $this->configurationStructureService = $configurationStructureService; $this->connection = $connection; $this->findViableNodesService = $findViableNodesService; @@ -130,15 +123,12 @@ class ServerCreationService * @throws \Throwable * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Illuminate\Validation\ValidationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException */ public function handle(array $data, DeploymentObject $deployment = null): Server { - $this->connection->beginTransaction(); - // If a deployment object has been passed we need to get the allocation // that the server should use, and assign the node from that allocation. if ($deployment instanceof DeploymentObject) { @@ -149,37 +139,42 @@ class ServerCreationService // Auto-configure the node based on the selected allocation // if no node was defined. - if (is_null(Arr::get($data, 'node_id'))) { - $data['node_id'] = $this->getNodeFromAllocation($data['allocation_id']); + if (empty($data['node_id'])) { + Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.'); + + $data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id; } - if (is_null(Arr::get($data, 'nest_id'))) { - /** @var \Pterodactyl\Models\Egg $egg */ - $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(Arr::get($data, 'egg_id')); - $data['nest_id'] = $egg->nest_id; + if (empty($data['nest_id'])) { + Assert::false(empty($data['egg_id']), 'Expected a non-empty egg_id in server creation data.'); + + $data['nest_id'] = Egg::query()->findOrFail($data['egg_id'])->nest_id; } $eggVariableData = $this->validatorService ->setUserLevel(User::USER_LEVEL_ADMIN) ->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', [])); - // Create the server and assign any additional allocations to it. - $server = $this->createModel($data); - - $this->storeAssignedAllocations($server, $data); - $this->storeEggVariables($server, $eggVariableData); - // Due to the design of the Daemon, we need to persist this server to the disk // before we can actually create it on the Daemon. // // If that connection fails out we will attempt to perform a cleanup by just // deleting the server itself from the system. - $this->connection->commit(); + /** @var \Pterodactyl\Models\Server $server */ + $server = $this->connection->transaction(function () use ($data, $eggVariableData) { + // Create the server and assign any additional allocations to it. + $server = $this->createModel($data); - $structure = $this->configurationStructureService->handle($server); + $this->storeAssignedAllocations($server, $data); + $this->storeEggVariables($server, $eggVariableData); + + return $server; + }); try { - $this->daemonServerRepository->setServer($server)->create($structure); + $this->daemonServerRepository->setServer($server)->create( + $this->configurationStructureService->handle($server) + ); } catch (DaemonConnectionException $exception) { $this->serverDeletionService->withForce(true)->handle($server); @@ -208,7 +203,7 @@ class ServerCreationService ->handle(); return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) - ->setNodes($nodes) + ->setNodes($nodes->pluck('id')->toArray()) ->setPorts($deployment->getPorts()) ->handle(); } @@ -269,7 +264,7 @@ class ServerCreationService $records = array_merge($records, $data['allocation_additional']); } - $this->allocationRepository->updateWhereIn('id', $records, [ + Allocation::query()->whereIn('id', $records)->update([ 'server_id' => $server->id, ]); } @@ -295,22 +290,6 @@ class ServerCreationService } } - /** - * Get the node that an allocation belongs to. - * - * @param int $id - * @return int - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - private function getNodeFromAllocation(int $id): int - { - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = $this->allocationRepository->setColumns(['id', 'node_id'])->find($id); - - return $allocation->node_id; - } - /** * Create a unique UUID and UUID-Short combo for a server. * diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index 8d7217769..95585873b 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -3,11 +3,10 @@ namespace Pterodactyl\Services\Servers; use Exception; -use Psr\Log\LoggerInterface; +use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Illuminate\Support\Facades\Log; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Repositories\Eloquent\ServerRepository; -use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; @@ -29,50 +28,26 @@ class ServerDeletionService */ private $daemonServerRepository; - /** - * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository - */ - private $databaseRepository; - /** * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ private $databaseManagementService; - /** - * @var \Pterodactyl\Repositories\Eloquent\ServerRepository - */ - private $repository; - - /** - * @var \Psr\Log\LoggerInterface - */ - private $writer; - /** * DeletionService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository - * @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $databaseRepository * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService - * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository - * @param \Psr\Log\LoggerInterface $writer */ public function __construct( ConnectionInterface $connection, DaemonServerRepository $daemonServerRepository, - DatabaseRepository $databaseRepository, - DatabaseManagementService $databaseManagementService, - ServerRepository $repository, - LoggerInterface $writer + DatabaseManagementService $databaseManagementService ) { $this->connection = $connection; $this->daemonServerRepository = $daemonServerRepository; - $this->databaseRepository = $databaseRepository; $this->databaseManagementService = $databaseManagementService; - $this->repository = $repository; - $this->writer = $writer; } /** @@ -101,27 +76,39 @@ class ServerDeletionService try { $this->daemonServerRepository->setServer($server)->delete(); } catch (DaemonConnectionException $exception) { - if ($this->force) { - $this->writer->warning($exception); - } else { + // If there is an error not caused a 404 error and this isn't a forced delete, + // go ahead and bail out. We specifically ignore a 404 since that can be assumed + // to be a safe error, meaning the server doesn't exist at all on Wings so there + // is no reason we need to bail out from that. + if (! $this->force && $exception->getStatusCode() !== Response::HTTP_NOT_FOUND) { throw $exception; } + + Log::warning($exception); } $this->connection->transaction(function () use ($server) { - $this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { + foreach ($server->databases as $database) { try { - $this->databaseManagementService->delete($item->id); + $this->databaseManagementService->delete($database); } catch (Exception $exception) { - if ($this->force) { - $this->writer->warning($exception); - } else { + if (!$this->force) { throw $exception; } - } - }); - $this->repository->delete($server->id); + // Oh well, just try to delete the database entry we have from the database + // so that the server itself can be deleted. This will leave it dangling on + // the host instance, but we couldn't delete it anyways so not sure how we would + // handle this better anyways. + // + // @see https://github.com/pterodactyl/panel/issues/2085 + $database->delete(); + + Log::warning($exception); + } + } + + $server->delete(); }); } } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 7bec8fac6..f154c54bd 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -2,13 +2,13 @@ namespace Pterodactyl\Services\Servers; +use Illuminate\Support\Arr; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; +use Pterodactyl\Models\ServerVariable; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Traits\Services\HasUserLevels; -use Pterodactyl\Contracts\Repository\EggRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class StartupModificationService { @@ -19,63 +19,21 @@ class StartupModificationService */ private $connection; - /** - * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface - */ - private $eggRepository; - - /** - * @var \Pterodactyl\Services\Servers\EnvironmentService - */ - private $environmentService; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - private $repository; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface - */ - private $serverVariableRepository; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ private $validatorService; - /** - * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService - */ - private $structureService; - /** * StartupModificationService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository - * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService - * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ - public function __construct( - ConnectionInterface $connection, - EggRepositoryInterface $eggRepository, - EnvironmentService $environmentService, - ServerRepositoryInterface $repository, - ServerConfigurationStructureService $structureService, - ServerVariableRepositoryInterface $serverVariableRepository, - VariableValidatorService $validatorService - ) { + public function __construct(ConnectionInterface $connection, VariableValidatorService $validatorService) + { $this->connection = $connection; - $this->eggRepository = $eggRepository; - $this->environmentService = $environmentService; - $this->repository = $repository; - $this->serverVariableRepository = $serverVariableRepository; $this->validatorService = $validatorService; - $this->structureService = $structureService; } /** @@ -85,34 +43,42 @@ class StartupModificationService * @param array $data * @return \Pterodactyl\Models\Server * - * @throws \Illuminate\Validation\ValidationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function handle(Server $server, array $data): Server { - $this->connection->beginTransaction(); - if (! is_null(array_get($data, 'environment'))) { - $this->validatorService->setUserLevel($this->getUserLevel()); - $results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', [])); + return $this->connection->transaction(function () use ($server, $data) { + if (! empty($data['environment'])) { + $egg = $this->isUserLevel(User::USER_LEVEL_ADMIN) ? ($data['egg_id'] ?? $server->egg_id) : $server->egg_id; - $results->each(function ($result) use ($server) { - $this->serverVariableRepository->withoutFreshModel()->updateOrCreate([ - 'server_id' => $server->id, - 'variable_id' => $result->id, - ], [ - 'variable_value' => $result->value ?? '', - ]); - }); - } + $results = $this->validatorService + ->setUserLevel($this->getUserLevel()) + ->handle($egg, $data['environment']); - if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { - $this->updateAdministrativeSettings($data, $server); - } + foreach ($results as $result) { + ServerVariable::query()->updateOrCreate( + [ + 'server_id' => $server->id, + 'variable_id' => $result->id, + ], + ['variable_value' => $result->value ?? ''] + ); + } + } - $this->connection->commit(); + if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { + $this->updateAdministrativeSettings($data, $server); + } - return $server; + // Calling ->refresh() rather than ->fresh() here causes it to return the + // variables as triplicates for some reason? Not entirely sure, should dig + // in more to figure it out, but luckily we have a test case covering this + // specific call so we can be assured we're not breaking it _here_ at least. + // + // TODO(dane): this seems like a red-flag for the code powering the relationship + // that should be looked into more. + return $server->fresh(); + }); } /** @@ -120,28 +86,25 @@ class StartupModificationService * * @param array $data * @param \Pterodactyl\Models\Server $server - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - private function updateAdministrativeSettings(array $data, Server &$server) + protected function updateAdministrativeSettings(array $data, Server &$server) { - if ( - is_digit(array_get($data, 'egg_id')) - && $data['egg_id'] != $server->egg_id - && is_null(array_get($data, 'nest_id')) - ) { - $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']); - $data['nest_id'] = $egg->nest_id; + $eggId = Arr::get($data, 'egg_id'); + + if (is_digit($eggId) && $server->egg_id !== (int)$eggId) { + /** @var \Pterodactyl\Models\Egg $egg */ + $egg = Egg::query()->findOrFail($data['egg_id']); + + $server = $server->forceFill([ + 'egg_id' => $egg->id, + 'nest_id' => $egg->nest_id, + ]); } - $server = $this->repository->update($server->id, [ - 'installed' => 0, - 'startup' => array_get($data, 'startup', $server->startup), - 'nest_id' => array_get($data, 'nest_id', $server->nest_id), - 'egg_id' => array_get($data, 'egg_id', $server->egg_id), - 'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']), - 'image' => array_get($data, 'docker_image', $server->image), - ]); + $server->fill([ + 'startup' => $data['startup'] ?? $server->startup, + 'skip_scripts' => $data['skip_scripts'] ?? isset($data['skip_scripts']), + 'image' => $data['docker_image'] ?? $server->image, + ])->save(); } } diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 9fb95645d..6497d73ed 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -2,12 +2,10 @@ namespace Pterodactyl\Services\Servers; -use Psr\Log\LoggerInterface; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Repositories\Wings\DaemonServerRepository; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class SuspensionService { @@ -19,16 +17,6 @@ class SuspensionService */ private $connection; - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - private $repository; - - /** - * @var \Psr\Log\LoggerInterface - */ - private $writer; - /** * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ @@ -39,25 +27,19 @@ class SuspensionService * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - * @param \Psr\Log\LoggerInterface $writer */ public function __construct( ConnectionInterface $connection, - DaemonServerRepository $daemonServerRepository, - ServerRepositoryInterface $repository, - LoggerInterface $writer + DaemonServerRepository $daemonServerRepository ) { $this->connection = $connection; - $this->repository = $repository; - $this->writer = $writer; $this->daemonServerRepository = $daemonServerRepository; } /** * Suspends a server on the system. * - * @param int|\Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @param string $action * * @throws \Throwable @@ -66,15 +48,16 @@ class SuspensionService { Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]); - if ( - $action === self::ACTION_SUSPEND && $server->suspended || - $action === self::ACTION_UNSUSPEND && ! $server->suspended - ) { + $isSuspending = $action === self::ACTION_SUSPEND; + // Nothing needs to happen if we're suspending the server and it is already + // suspended in the database. Additionally, nothing needs to happen if the server + // is not suspended and we try to un-suspend the instance. + if ($isSuspending === $server->suspended) { return; } $this->connection->transaction(function () use ($action, $server) { - $this->repository->withoutFreshModel()->update($server->id, [ + $server->update([ 'suspended' => $action === self::ACTION_SUSPEND, ]); diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 5fa0607f6..7cb1aa427 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -11,32 +11,15 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\User; use Illuminate\Support\Collection; +use Pterodactyl\Models\EggVariable; use Illuminate\Validation\ValidationException; use Pterodactyl\Traits\Services\HasUserLevels; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Illuminate\Contracts\Validation\Factory as ValidationFactory; -use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorService { use HasUserLevels; - /** - * @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface - */ - private $optionVariableRepository; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface - */ - private $serverRepository; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface - */ - private $serverVariableRepository; - /** * @var \Illuminate\Contracts\Validation\Factory */ @@ -45,20 +28,10 @@ class VariableValidatorService /** * VariableValidatorService constructor. * - * @param \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface $optionVariableRepository - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository - * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Illuminate\Contracts\Validation\Factory $validator */ - public function __construct( - EggVariableRepositoryInterface $optionVariableRepository, - ServerRepositoryInterface $serverRepository, - ServerVariableRepositoryInterface $serverVariableRepository, - ValidationFactory $validator - ) { - $this->optionVariableRepository = $optionVariableRepository; - $this->serverRepository = $serverRepository; - $this->serverVariableRepository = $serverVariableRepository; + public function __construct(ValidationFactory $validator) + { $this->validator = $validator; } @@ -72,16 +45,18 @@ class VariableValidatorService */ public function handle(int $egg, array $fields = []): Collection { - $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]); + $query = EggVariable::query()->where('egg_id', $egg); + if (! $this->isUserLevel(User::USER_LEVEL_ADMIN)) { + // Don't attempt to validate variables if they aren't user editable + // and we're not running this at an admin level. + $query = $query->where('user_editable', true)->where('user_viewable', true); + } + + /** @var \Pterodactyl\Models\EggVariable[] $variables */ + $variables = $query->get(); $data = $rules = $customAttributes = []; foreach ($variables as $variable) { - // Don't attempt to validate variables if they aren't user editable - // and we're not running this at an admin level. - if (! $variable->user_editable && ! $this->isUserLevel(User::USER_LEVEL_ADMIN)) { - continue; - } - $data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable); $rules['environment.' . $variable->env_variable] = $variable->rules; $customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]); @@ -92,23 +67,12 @@ class VariableValidatorService throw new ValidationException($validator); } - $response = $variables->filter(function ($item) { - // Skip doing anything if user is not an admin and variable is not user viewable or editable. - if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) { - return false; - } - - return true; - })->map(function ($item) use ($fields) { - return (object) [ + return Collection::make($variables)->map(function ($item) use ($fields) { + return (object)[ 'id' => $item->id, 'key' => $item->env_variable, - 'value' => array_get($fields, $item->env_variable), + 'value' => $fields[$item->env_variable] ?? null, ]; - })->filter(function ($item) { - return is_object($item); }); - - return $response; } } diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index d40bfd3f6..92b9ea0bf 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -67,7 +67,7 @@ class ServerTransformer extends BaseClientTransformer 'allocations' => $server->allocation_limit, 'backups' => $server->backup_limit, ], - 'is_suspended' => $server->suspended !== 0, + 'is_suspended' => $server->suspended, 'is_installing' => $server->installed !== 1, ]; } diff --git a/config/app.php b/config/app.php index f57036f95..f1fb42d83 100644 --- a/config/app.php +++ b/config/app.php @@ -9,7 +9,7 @@ return [ | change this value if you are not maintaining your own internal versions. */ - 'version' => 'canary', + 'version' => '1.0.1', /* |-------------------------------------------------------------------------- diff --git a/config/http.php b/config/http.php index b39845239..ed54e475b 100644 --- a/config/http.php +++ b/config/http.php @@ -13,7 +13,7 @@ return [ */ 'rate_limit' => [ 'client_period' => 1, - 'client' => env('APP_API_CLIENT_RATELIMIT', 240), + 'client' => env('APP_API_CLIENT_RATELIMIT', 720), 'application_period' => 1, 'application' => env('APP_API_APPLICATION_RATELIMIT', 240), diff --git a/config/logging.php b/config/logging.php index 7da06f407..bb4470c0f 100644 --- a/config/logging.php +++ b/config/logging.php @@ -1,5 +1,6 @@ 'errorlog', 'level' => 'debug', ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], ], ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 0525c30be..4997a9b6f 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -1,5 +1,6 @@ define(Pterodactyl\Models\Server::class, function (Faker $faker) { 'installed' => 1, 'database_limit' => null, 'allocation_limit' => null, - 'created_at' => \Carbon\Carbon::now(), - 'updated_at' => \Carbon\Carbon::now(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), ]; }); @@ -160,8 +162,8 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) { 'username' => str_random(10), 'remote' => '%', 'password' => $password ?: bcrypt('test123'), - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), ]; }); @@ -190,7 +192,7 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) { 'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)), 'allowed_ips' => null, 'memo' => 'Test Function Key', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), ]; }); diff --git a/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php b/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php index 621c9526b..67461ecc8 100644 --- a/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php +++ b/database/migrations/2020_03_22_163911_merge_permissions_table_into_subusers.php @@ -1,12 +1,64 @@ P::ACTION_CONTROL_START, + 'power-stop' => P::ACTION_CONTROL_STOP, + 'power-restart' => P::ACTION_CONTROL_RESTART, + 'power-kill' => P::ACTION_CONTROL_STOP, + 'send-command' => P::ACTION_CONTROL_CONSOLE, + 'list-subusers' => P::ACTION_USER_READ, + 'view-subuser' => P::ACTION_USER_READ, + 'edit-subuser' => P::ACTION_USER_UPDATE, + 'create-subuser' => P::ACTION_USER_CREATE, + 'delete-subuser' => P::ACTION_USER_DELETE, + 'view-allocations' => P::ACTION_ALLOCATION_READ, + 'edit-allocation' => P::ACTION_ALLOCATION_UPDATE, + 'view-startup' => P::ACTION_STARTUP_READ, + 'edit-startup' => P::ACTION_STARTUP_UPDATE, + 'view-databases' => P::ACTION_DATABASE_READ, + // Better to just break this flow a bit than accidentally grant a dangerous permission. + 'reset-db-password' => P::ACTION_DATABASE_UPDATE, + 'delete-database' => P::ACTION_DATABASE_DELETE, + 'create-database' => P::ACTION_DATABASE_CREATE, + 'access-sftp' => P::ACTION_FILE_SFTP, + 'list-files' => P::ACTION_FILE_READ, + 'edit-files' => P::ACTION_FILE_READ_CONTENT, + 'save-files' => P::ACTION_FILE_UPDATE, + 'create-files' => P::ACTION_FILE_CREATE, + 'delete-files' => P::ACTION_FILE_DELETE, + 'compress-files' => P::ACTION_FILE_ARCHIVE, + 'list-schedules' => P::ACTION_SCHEDULE_READ, + 'view-schedule' => P::ACTION_SCHEDULE_READ, + 'edit-schedule' => P::ACTION_SCHEDULE_UPDATE, + 'create-schedule' => P::ACTION_SCHEDULE_CREATE, + 'delete-schedule' => P::ACTION_SCHEDULE_DELETE, + // Skipping these permissions as they are granted if you have more specific read/write permissions. + 'move-files' => null, + 'copy-files' => null, + 'decompress-files' => null, + 'upload-files' => null, + 'download-files' => null, + // These permissions do not exist in 1.0 + 'toggle-schedule' => null, + 'queue-schedule' => null, + ]; + /** * Run the migrations. * @@ -27,10 +79,19 @@ class MergePermissionsTableIntoSubusers extends Migration DB::transaction(function () use (&$cursor) { $cursor->each(function ($datum) { - DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [ - json_encode(explode(',', $datum->permissions)), - $datum->subuser_id, - ]); + $updated = Collection::make(explode(',', $datum->permissions)) + ->map(function ($value) { + return self::$permissionsMap[$value] ?? null; + })->filter(function ($value) { + return !is_null($value) && $value !== Permission::ACTION_WEBSOCKET_CONNECT; + }) + // All subusers get this permission, so make sure it gets pushed into the array. + ->merge([ Permission::ACTION_WEBSOCKET_CONNECT ]) + ->unique() + ->values() + ->toJson(); + + DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [$updated, $datum->subuser_id]); }); }); } @@ -42,11 +103,15 @@ class MergePermissionsTableIntoSubusers extends Migration */ public function down() { + $flipped = array_flip(self::$permissionsMap); + foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) { $values = []; foreach (json_decode($datum->permissions, true) as $permission) { - $values[] = $datum->id; - $values[] = $permission; + if (!empty($v = $flipped[$permission])) { + $values[] = $datum->id; + $values[] = $v; + } } if (! empty($values)) { diff --git a/database/migrations/2020_04_04_131016_add_table_server_transfers.php b/database/migrations/2020_04_04_131016_add_table_server_transfers.php index ae0a57ca9..096b5384f 100644 --- a/database/migrations/2020_04_04_131016_add_table_server_transfers.php +++ b/database/migrations/2020_04_04_131016_add_table_server_transfers.php @@ -13,6 +13,10 @@ class AddTableServerTransfers extends Migration */ public function up() { + // Nuclear approach to whatever plugins are out there and not properly namespacing their own tables + // leading to constant support requests from people... + Schema::dropIfExists('server_transfers'); + Schema::create('server_transfers', function (Blueprint $table) { $table->increments('id'); $table->integer('server_id')->unsigned(); diff --git a/database/migrations/2020_10_10_165437_change_unique_database_name_to_account_for_server.php b/database/migrations/2020_10_10_165437_change_unique_database_name_to_account_for_server.php new file mode 100644 index 000000000..a32d52e6e --- /dev/null +++ b/database/migrations/2020_10_10_165437_change_unique_database_name_to_account_for_server.php @@ -0,0 +1,41 @@ +dropUnique(['database_host_id', 'database']); + }); + + Schema::table('databases', function (Blueprint $table) { + $table->unique(['database_host_id', 'server_id', 'database']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('databases', function (Blueprint $table) { + $table->dropUnique(['database_host_id', 'server_id', 'database']); + }); + + Schema::table('databases', function (Blueprint $table) { + $table->unique(['database_host_id', 'database']); + }); + + } +} diff --git a/database/seeds/EggSeeder.php b/database/seeds/EggSeeder.php index c532d556c..dea5b675e 100644 --- a/database/seeds/EggSeeder.php +++ b/database/seeds/EggSeeder.php @@ -130,7 +130,7 @@ class EggSeeder extends Seeder ['nest_id', '=', $nest->id], ]); - $this->updateImporterService->handle($egg->id, $file); + $this->updateImporterService->handle($egg, $file); $this->command->info('Updated ' . $decoded->name); } catch (RecordNotFoundException $exception) { diff --git a/public/themes/pterodactyl/js/admin/new-server.js b/public/themes/pterodactyl/js/admin/new-server.js index 7416451da..cda0d5cf3 100644 --- a/public/themes/pterodactyl/js/admin/new-server.js +++ b/public/themes/pterodactyl/js/admin/new-server.js @@ -153,6 +153,12 @@ function updateAdditionalAllocations() { } function initUserIdSelect(data) { + function escapeHtml(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + } + $('#pUserId').select2({ ajax: { url: '/admin/users/accounts.json', @@ -176,28 +182,27 @@ function initUserIdSelect(data) { data: data, escapeMarkup: function (markup) { return markup; }, minimumInputLength: 2, - templateResult: function (data) { - if (data.loading) return data.text; + if (data.loading) return escapeHtml(data.text); return '
\ - User Image \ - \ - ' + data.name_first + ' ' + data.name_last +' \ - \ - ' + data.email + ' - ' + data.username + ' \ -
'; + User Image \ + \ + ' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) +' \ + \ + ' + escapeHtml(data.email) + ' - ' + escapeHtml(data.username) + ' \ + '; }, - templateSelection: function (data) { return '
\ - \ - User Image \ - \ - \ - ' + data.name_first + ' ' + data.name_last + ' (' + data.email + ') \ - \ -
'; + \ + User Image \ + \ + \ + ' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) + ' (' + escapeHtml(data.email) + ') \ + \ + '; } + }); } diff --git a/resources/scripts/api/swr/getServerStartup.ts b/resources/scripts/api/swr/getServerStartup.ts index fff0263f9..892f78fdd 100644 --- a/resources/scripts/api/swr/getServerStartup.ts +++ b/resources/scripts/api/swr/getServerStartup.ts @@ -9,7 +9,6 @@ interface Response { } export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise => { - console.log('firing getServerStartup'); const { data } = await http.get(`/api/client/servers/${uuid}/startup`); const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable); diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx index 82bd5e5ff..f82b76e20 100644 --- a/resources/scripts/components/auth/ForgotPasswordContainer.tsx +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -94,7 +94,6 @@ export default () => { }
diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index 33f0eb561..d739ea601 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -14,26 +14,8 @@ import { httpErrorToHuman } from '@/api/http'; import { format } from 'date-fns'; import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; -import { breakpoint } from '@/theme'; -import styled from 'styled-components/macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; -const Container = styled.div` - ${tw`flex flex-wrap my-10`}; - - & > div { - ${tw`w-full`}; - - ${breakpoint('md')` - width: calc(50% - 1rem); - `} - - ${breakpoint('xl')` - ${tw`w-auto flex-1`}; - `} - } -`; - export default () => { const [ deleteIdentifier, setDeleteIdentifier ] = useState(''); const [ keys, setKeys ] = useState([]); @@ -67,12 +49,12 @@ export default () => { return ( - - - + +
+ setKeys(s => ([ ...s!, key ]))}/> - + { css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]} > -
-

{key.description}

+
+

{key.description}

Last used:  {key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}

-

+

); }; diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index b9979858a..7373fa246 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -1,21 +1,43 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { memo, useEffect, useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons'; import { Link } from 'react-router-dom'; import { Server } from '@/api/server/getServer'; -import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage'; +import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage'; import { bytesToHuman, megabytesToHuman } from '@/helpers'; import tw from 'twin.macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; import Spinner from '@/components/elements/Spinner'; +import styled from 'styled-components/macro'; +import isEqual from 'react-fast-compare'; // Determines if the current value is in an alarm threshold so we can show it in red rather // than the more faded default style. -const isAlarmState = (current: number, limit: number): boolean => { - const limitInBytes = limit * 1024 * 1024; +const isAlarmState = (current: number, limit: number): boolean => limit > 0 && (current / (limit * 1024 * 1024) >= 0.90); - return current / limitInBytes >= 0.90; -}; +const Icon = memo(styled(FontAwesomeIcon)<{ $alarm: boolean }>` + ${props => props.$alarm ? tw`text-red-400` : tw`text-neutral-500`}; +`, isEqual); + +const IconDescription = styled.p<{ $alarm: boolean }>` + ${tw`text-sm ml-2`}; + ${props => props.$alarm ? tw`text-white` : tw`text-neutral-400`}; +`; + +const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>` + ${tw`grid grid-cols-12 gap-4 relative`}; + + & .status-bar { + ${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`}; + height: calc(100% - 0.5rem); + + ${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)}; + } + + &:hover .status-bar { + ${tw`opacity-75`}; + } +`; export default ({ server, className }: { server: Server; className?: string }) => { const interval = useRef(null); @@ -54,29 +76,31 @@ export default ({ server, className }: { server: Server; className?: string }) = const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited'; return ( - -
- -
-
-

{server.name}

- {!!server.description && -

{server.description}

- } -
-