diff --git a/app/Contracts/Repository/EggVariableRepositoryInterface.php b/app/Contracts/Repository/EggVariableRepositoryInterface.php index afaf7463b..77b46f96d 100644 --- a/app/Contracts/Repository/EggVariableRepositoryInterface.php +++ b/app/Contracts/Repository/EggVariableRepositoryInterface.php @@ -9,6 +9,16 @@ namespace Pterodactyl\Contracts\Repository; +use Illuminate\Support\Collection; + interface EggVariableRepositoryInterface extends RepositoryInterface { + /** + * Return editable variables for a given egg. Editable variables must be set to + * user viewable in order to be picked up by this function. + * + * @param int $egg + * @return \Illuminate\Support\Collection + */ + public function getEditableVariables(int $egg): Collection; } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 73cb5c71e..7031b27e6 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface @@ -52,6 +53,19 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter */ public function getVariablesWithValues($id, $returnAsObject = false); + /** + * Get the primary allocation for a given server. If a model is passed into + * the function, load the allocation relationship onto it. Otherwise, find and + * return the server from the database. + * + * @param int|\Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getPrimaryAllocation($server, bool $refresh = false): Server; + /** * Return enough data to be used for the creation of a server via the daemon. * diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php deleted file mode 100644 index 65700b24f..000000000 --- a/app/Http/Controllers/Server/ServerController.php +++ /dev/null @@ -1,163 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Server; - -use Log; -use Alert; -use Pterodactyl\Models; -use Illuminate\Http\Request; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; - -class ServerController extends Controller -{ - /** - * Returns the allocation overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getAllocation(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-allocation', $server); - $server->js(); - - return view('server.settings.allocation', [ - 'server' => $server->load(['allocations' => function ($query) { - $query->orderBy('ip', 'asc'); - $query->orderBy('port', 'asc'); - }]), - 'node' => $server->node, - ]); - } - - /** - * Returns the startup overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getStartup(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-startup', $server); - - $server->load(['node', 'allocation', 'variables']); - $variables = Models\EggVariable::where('option_id', $server->option_id)->get(); - - $replacements = [ - '{{SERVER_MEMORY}}' => $server->memory, - '{{SERVER_IP}}' => $server->allocation->ip, - '{{SERVER_PORT}}' => $server->allocation->port, - ]; - - $processed = str_replace(array_keys($replacements), array_values($replacements), $server->startup); - - foreach ($variables as $var) { - if ($var->user_viewable) { - $serverVar = $server->variables->where('variable_id', $var->id)->first(); - $var->server_set_value = $serverVar->variable_value ?? $var->default_value; - } else { - $var->server_set_value = '[hidden]'; - } - - $processed = str_replace('{{' . $var->env_variable . '}}', $var->server_set_value, $processed); - } - - $server->js(); - - return view('server.settings.startup', [ - 'server' => $server, - 'node' => $server->node, - 'variables' => $variables->where('user_viewable', 1), - 'service' => $server->service, - 'processedStartup' => $processed, - ]); - } - - /** - * Returns the SFTP overview for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getSFTP(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('view-sftp', $server); - $server->js(); - - return view('server.settings.sftp', [ - 'server' => $server, - 'node' => $server->node, - ]); - } - - /** - * Handles changing the SFTP password for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\RedirectResponse - */ - public function postSettingsSFTP(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('reset-sftp', $server); - - try { - $repo = new ServerRepository; - $repo->updateSFTPPassword($server->id, $request->input('sftp_pass')); - Alert::success('Successfully updated this servers SFTP password.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('server.settings.sftp', $uuid)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unknown error occured while attempting to update this server\'s SFTP settings.')->flash(); - } - - return redirect()->route('server.settings.sftp', $uuid); - } - - /** - * Handles changing the startup settings for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\RedirectResponse - */ - public function postSettingsStartup(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('edit-startup', $server); - - try { - $repo = new ServerRepository; - $repo->updateStartup($server->id, $request->except('_token')); - Alert::success('Server startup variables were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('server.settings.startup', $uuid)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. Please try again.')->flash(); - } - - return redirect()->route('server.settings.startup', $uuid); - } -} diff --git a/app/Http/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php new file mode 100644 index 000000000..f5ea20a47 --- /dev/null +++ b/app/Http/Controllers/Server/Settings/StartupController.php @@ -0,0 +1,92 @@ +alert = $alert; + $this->commandViewService = $commandViewService; + $this->modificationService = $modificationService; + } + + /** + * Render the server startup page. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index(Request $request) + { + $server = $request->attributes->get('server'); + $this->authorize('view-startup', $server); + $this->injectJavascript(); + + $data = $this->commandViewService->handle($server->id); + + return view('server.settings.startup', [ + 'variables' => $data->get('variables'), + 'server_values' => $data->get('server_values'), + 'startup' => $data->get('startup'), + ]); + } + + /** + * Handle request to update the startup variables for a server. Authorization + * is handled in the form request. + * + * @param \Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateStartupParametersFormRequest $request): RedirectResponse + { + $this->modificationService->handle($request->attributes->get('server'), $request->normalize()); + $this->alert->success(trans('server.config.startup.edited'))->flash(); + + return redirect()->route('server.settings.startup', ['server' => $request->attributes->get('server')->uuidShort]); + } +} diff --git a/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php b/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php new file mode 100644 index 000000000..41c15103f --- /dev/null +++ b/app/Http/Requests/Server/UpdateStartupParametersFormRequest.php @@ -0,0 +1,61 @@ +user()->can('edit-startup', $this->attributes->get('server')); + } + + /** + * Validate that all of the required fields were passed and that the environment + * variable values meet the defined criteria for those fields. + * + * @return array + */ + public function rules() + { + $repository = $this->container->make(EggVariableRepositoryInterface::class); + + $variables = $repository->getEditableVariables($this->attributes->get('server')->egg_id); + $rules = $variables->mapWithKeys(function ($variable) { + $this->validationAttributes['environment.' . $variable->env_variable] = $variable->name; + + return ['environment.' . $variable->env_variable => $variable->rules]; + })->toArray(); + + return array_merge($rules, [ + 'environment' => 'required|array', + ]); + } + + /** + * Return attributes to provide better naming conventions for error messages. + * + * @return array + */ + public function attributes() + { + return $this->validationAttributes; + } +} diff --git a/app/Repositories/Eloquent/EggVariableRepository.php b/app/Repositories/Eloquent/EggVariableRepository.php index 9fe1174b4..2c34c7527 100644 --- a/app/Repositories/Eloquent/EggVariableRepository.php +++ b/app/Repositories/Eloquent/EggVariableRepository.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; @@ -21,4 +22,20 @@ class EggVariableRepository extends EloquentRepository implements EggVariableRep { return EggVariable::class; } + + /** + * Return editable variables for a given egg. Editable variables must be set to + * user viewable in order to be picked up by this function. + * + * @param int $egg + * @return \Illuminate\Support\Collection + */ + public function getEditableVariables(int $egg): Collection + { + return $this->getBuilder()->where([ + ['egg_id', '=', $egg], + ['user_viewable', '=', 1], + ['user_editable', '=', 1], + ])->get($this->getColumns()); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 10805fdea..da15c89b1 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -67,8 +67,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt Assert::integerish($id, 'First argument passed to findWithVariables must be integer, received %s.'); $instance = $this->getBuilder()->with('egg.variables', 'variables') - ->where($this->getModel()->getKeyName(), '=', $id) - ->first($this->getColumns()); + ->where($this->getModel()->getKeyName(), '=', $id) + ->first($this->getColumns()); if (is_null($instance)) { throw new RecordNotFoundException(); @@ -77,6 +77,36 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt return $instance; } + /** + * Get the primary allocation for a given server. If a model is passed into + * the function, load the allocation relationship onto it. Otherwise, find and + * return the server from the database. + * + * @param int|\Pterodactyl\Models\Server $server + * @param bool $refresh + * @return \Pterodactyl\Models\Server + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getPrimaryAllocation($server, bool $refresh = false): Server + { + $instance = $server; + if (! $instance instanceof Server) { + Assert::integerish($server, 'First argument passed to getPrimaryAllocation must be instance of \Pterodactyl\Models\Server or integer, received %s.'); + $instance = $this->getBuilder()->find($server, $this->getColumns()); + } + + if (! $instance) { + throw new RecordNotFoundException; + } + + if (! $instance->relationLoaded('allocation') || $refresh) { + $instance->load('allocation'); + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/Services/Servers/StartupCommandViewService.php b/app/Services/Servers/StartupCommandViewService.php new file mode 100644 index 000000000..14a4cc3c3 --- /dev/null +++ b/app/Services/Servers/StartupCommandViewService.php @@ -0,0 +1,55 @@ +repository = $repository; + } + + /** + * Generate a startup command for a server and return all of the user-viewable variables + * as well as thier assigned values. + * + * @param int $server + * @return \Illuminate\Support\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(int $server): Collection + { + $response = $this->repository->getVariablesWithValues($server, true); + $server = $this->repository->getPrimaryAllocation($response->server); + + $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; + $replace = [$server->memory, $server->allocation->ip, $server->allocation->port]; + + $variables = $server->egg->variables->each(function ($variable) use (&$find, &$replace, $response) { + $find[] = '{{' . $variable->env_variable . '}}'; + $replace[] = $variable->user_viewable ? $response->data[$variable->env_variable] : '[hidden]'; + })->filter(function ($variable) { + return $variable->user_viewable === 1; + }); + + return collect([ + 'startup' => str_replace($find, $replace, $server->startup), + 'variables' => $variables, + 'server_values' => $response->data, + ]); + } +} diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 25650b1c0..78460c197 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -12,8 +12,8 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -118,9 +118,9 @@ class StartupModificationService } $this->connection->beginTransaction(); - if (isset($data['environment'])) { + if (! is_null(array_get($data, 'environment'))) { $validator = $this->validatorService->isAdmin($this->admin) - ->setFields($data['environment']) + ->setFields(array_get($data, 'environment', [])) ->validate(array_get($data, 'egg_id', $server->egg_id)); foreach ($validator->getResults() as $result) { @@ -159,12 +159,11 @@ class StartupModificationService try { $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); - $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ]), $exception, 'warning'); + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); } + + $this->connection->commit(); } } diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 3ed89c511..3aa27e5b2 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -292,8 +292,8 @@ return [ 'command' => 'Startup Command', 'edit_params' => 'Edit Parameters', 'update' => 'Update Startup Parameters', - 'startup_var' => 'Startup Command Variable', 'startup_regex' => 'Input Rules', + 'edited' => 'Startup variables have been successfully edited. They will take effect the next time this server is started.', ], 'sftp' => [ 'header' => 'SFTP Configuration', diff --git a/resources/themes/pterodactyl/server/settings/startup.blade.php b/resources/themes/pterodactyl/server/settings/startup.blade.php index ff52fae9b..594dae4d2 100644 --- a/resources/themes/pterodactyl/server/settings/startup.blade.php +++ b/resources/themes/pterodactyl/server/settings/startup.blade.php @@ -21,26 +21,20 @@ @section('content')