diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index b50d66a7..e662c7b1 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -26,5 +26,28 @@ namespace Pterodactyl\Contracts\Repository; interface ServiceOptionRepositoryInterface extends RepositoryInterface { - // + /** + * Return a service option with the variables relation attached. + * + * @param int $id + * @return mixed + */ + public function getWithVariables($id); + + /** + * Return a service option with the copyFrom relation loaded onto the model. + * + * @param int $id + * @return mixed + */ + public function getWithCopyFrom($id); + + /** + * Confirm a copy script belongs to the same service as the item trying to use it. + * + * @param int $copyFromId + * @param int $service + * @return bool + */ + public function isCopiableScript($copyFromId, $service); } diff --git a/app/Contracts/Repository/ServiceVariableRepositoryInterface.php b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php new file mode 100644 index 00000000..bcdc6196 --- /dev/null +++ b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Contracts\Repository; + +interface ServiceVariableRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ed83b200..4fb28768 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -5,7 +5,12 @@ namespace Pterodactyl\Exceptions; use Exception; use Prologue\Alerts\Facades\Alert; use Illuminate\Auth\AuthenticationException; +use Illuminate\Session\TokenMismatchException; +use Illuminate\Validation\ValidationException; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Exceptions\Model\DataValidationException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler @@ -16,15 +21,15 @@ class Handler extends ExceptionHandler * @var array */ protected $dontReport = [ - \Illuminate\Auth\AuthenticationException::class, - \Illuminate\Auth\Access\AuthorizationException::class, - \Symfony\Component\HttpKernel\Exception\HttpException::class, - \Illuminate\Database\Eloquent\ModelNotFoundException::class, - \Illuminate\Session\TokenMismatchException::class, - \Illuminate\Validation\ValidationException::class, + AuthenticationException::class, + AuthorizationException::class, DisplayException::class, - DisplayValidationException::class, DataValidationException::class, + DisplayValidationException::class, + HttpException::class, + ModelNotFoundException::class, + TokenMismatchException::class, + ValidationException::class, ]; /** diff --git a/app/Exceptions/Repository/RecordNotFoundException.php b/app/Exceptions/Repository/RecordNotFoundException.php index 932b83d1..1e9ac987 100644 --- a/app/Exceptions/Repository/RecordNotFoundException.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -24,9 +24,7 @@ namespace Pterodactyl\Exceptions\Repository; -use Illuminate\Database\Eloquent\ModelNotFoundException; - -class RecordNotFoundException extends ModelNotFoundException +class RecordNotFoundException extends \Exception { // } diff --git a/app/Exceptions/Services/Servers/RequiredVariableMissingException.php b/app/Exceptions/Services/Server/RequiredVariableMissingException.php similarity index 96% rename from app/Exceptions/Services/Servers/RequiredVariableMissingException.php rename to app/Exceptions/Services/Server/RequiredVariableMissingException.php index f4a1a631..bf166a62 100644 --- a/app/Exceptions/Services/Servers/RequiredVariableMissingException.php +++ b/app/Exceptions/Services/Server/RequiredVariableMissingException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\Servers; +namespace Pterodactyl\Exceptions\Services\Server; use Exception; diff --git a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php b/app/Exceptions/Services/ServiceOption/HasActiveServersException.php new file mode 100644 index 00000000..e1ea03b3 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/HasActiveServersException.php @@ -0,0 +1,30 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Exceptions\Services\ServiceOption; + +class HasActiveServersException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php b/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php new file mode 100644 index 00000000..34601313 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php @@ -0,0 +1,30 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Exceptions\Services\ServiceOption; + +class InvalidCopyFromException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php b/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php new file mode 100644 index 00000000..7d793504 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php @@ -0,0 +1,30 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Exceptions\Services\ServiceOption; + +class NoParentConfigurationFoundException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php b/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php new file mode 100644 index 00000000..9777b0e9 --- /dev/null +++ b/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php @@ -0,0 +1,32 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Exceptions\Services\ServiceVariable; + +use Exception; + +class ReservedVariableNameException extends Exception +{ + // +} diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 84954ef1..fa994d8b 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -24,26 +24,22 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; -use Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest; -use Pterodactyl\Services\Services\Options\CreationService; -use Pterodactyl\Services\Services\Variables\VariableCreationService; -use Route; use Javascript; use Illuminate\Http\Request; -use Pterodactyl\Models\Service; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\OptionRepository; -use Pterodactyl\Repositories\VariableRepository; -use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; -use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; +use Pterodactyl\Services\Services\Options\OptionUpdateService; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Services\Services\Options\OptionCreationService; +use Pterodactyl\Services\Services\Options\OptionDeletionService; +use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; +use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; +use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller { @@ -53,9 +49,24 @@ class OptionController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\Services\Options\CreationService + * @var \Pterodactyl\Services\Services\Options\InstallScriptUpdateService */ - protected $creationService; + protected $installScriptUpdateService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionCreationService + */ + protected $optionCreationService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionDeletionService + */ + protected $optionDeletionService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionUpdateService + */ + protected $optionUpdateService; /** * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface @@ -63,28 +74,37 @@ class OptionController extends Controller protected $serviceRepository; /** - * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface */ - protected $variableCreationService; + protected $serviceOptionRepository; /** * OptionController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository - * @param \Pterodactyl\Services\Services\Options\CreationService $creationService - * @param \Pterodactyl\Services\Services\Variables\VariableCreationService $variableCreationService + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Services\Options\InstallScriptUpdateService $installScriptUpdateService + * @param \Pterodactyl\Services\Services\Options\OptionCreationService $optionCreationService + * @param \Pterodactyl\Services\Services\Options\OptionDeletionService $optionDeletionService + * @param \Pterodactyl\Services\Services\Options\OptionUpdateService $optionUpdateService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository */ public function __construct( AlertsMessageBag $alert, + InstallScriptUpdateService $installScriptUpdateService, + OptionCreationService $optionCreationService, + OptionDeletionService $optionDeletionService, + OptionUpdateService $optionUpdateService, ServiceRepositoryInterface $serviceRepository, - CreationService $creationService, - VariableCreationService $variableCreationService + ServiceOptionRepositoryInterface $serviceOptionRepository ) { $this->alert = $alert; - $this->creationService = $creationService; + $this->installScriptUpdateService = $installScriptUpdateService; + $this->optionCreationService = $optionCreationService; + $this->optionDeletionService = $optionDeletionService; + $this->optionUpdateService = $optionUpdateService; $this->serviceRepository = $serviceRepository; - $this->variableCreationService = $variableCreationService; + $this->serviceOptionRepository = $serviceOptionRepository; } /** @@ -103,77 +123,77 @@ class OptionController extends Controller /** * Handle adding a new service option. * - * @param \Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest $request * @return \Illuminate\Http\RedirectResponse * - * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function store(ServiceOptionFormRequest $request) { - $option = $this->creationService->handle($request->normalize()); - $this->alert->success(trans('admin/services.options.notices.option_created'))->flash(); + try { + $option = $this->optionCreationService->handle($request->normalize()); + $this->alert->success(trans('admin/services.options.notices.option_created'))->flash(); + } catch (NoParentConfigurationFoundException $exception) { + $this->alert->danger($exception->getMessage())->flash(); + + return redirect()->back()->withInput(); + } return redirect()->route('admin.services.option.view', $option->id); } /** - * Handles POST request to create a new option variable. + * Delete a given option from the database. * - * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse */ - public function createVariable(OptionVariableFormRequest $request, ServiceOption $option) + public function delete(ServiceOption $option) { - $this->variableCreationService->handle($option->id, $request->normalize()); - $this->alert->success(trans('admin/services.variables.notices.variable_created'))->flash(); + try { + $this->optionDeletionService->handle($option->id); + $this->alert->success()->flash(); + } catch (HasActiveServersException $exception) { + $this->alert->danger($exception->getMessage())->flash(); - return redirect()->route('admin.services.option.variables', $option->id); + return redirect()->route('admin.services.option.view', $option->id); + } + + return redirect()->route('admin.services.view', $option->service_id); } /** * Display option overview page. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\View\View */ - public function viewConfiguration(Request $request, $id) + public function viewConfiguration(ServiceOption $option) { - return view('admin.services.options.view', ['option' => ServiceOption::findOrFail($id)]); - } - - /** - * Display variable overview page for a service option. - * - * @param \Illuminate\Http\Request $request - * @param int $id - * @return \Illuminate\View\View - */ - public function viewVariables(Request $request, $id) - { - return view('admin.services.options.variables', ['option' => ServiceOption::with('variables') - ->findOrFail($id), ]); + return view('admin.services.options.view', ['option' => $option]); } /** * Display script management page for an option. * - * @param Request $request - * @param int $id + * @param int $option * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function viewScripts(Request $request, $id) + public function viewScripts($option) { - $option = ServiceOption::with('copyFrom')->findOrFail($id); + $option = $this->serviceOptionRepository->getWithCopyFrom($option); + $copyOptions = $this->serviceOptionRepository->findWhere([ + ['copy_script_from', '=', null], + ['service_id', '=', $option->service_id], + ['id', '!=', $option], + ]); + $relyScript = $this->serviceOptionRepository->findWhere([['copy_script_from', '=', $option]]); return view('admin.services.options.scripts', [ - 'copyFromOptions' => ServiceOption::whereNull('copy_script_from')->where([ - ['service_id', $option->service_id], - ['id', '!=', $option->id], - ])->get(), - 'relyOnScript' => ServiceOption::where('copy_script_from', $option->id)->get(), + 'copyFromOptions' => $copyOptions, + 'relyOnScript' => $relyScript, 'option' => $option, ]); } @@ -181,92 +201,44 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse - */ - public function editConfiguration(Request $request, $id) - { - $repo = new OptionRepository; - - try { - if ($request->input('action') !== 'delete') { - $repo->update($id, $request->intersect([ - 'name', 'description', 'tag', 'docker_image', 'startup', - 'config_from', 'config_stop', 'config_logs', 'config_files', 'config_startup', - ])); - Alert::success('Service option configuration has been successfully updated.')->flash(); - } else { - $option = ServiceOption::with('service')->where('id', $id)->first(); - $repo->delete($id); - Alert::success('Successfully deleted service option from the system.')->flash(); - - return redirect()->route('admin.services.view', $option->service_id); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occurred while attempting to perform that action. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.services.option.view', $id); - } - - /** - * Handles POST when editing a configration for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable $request - * @param int $option - * @param int $variable - * @return \Illuminate\Http\RedirectResponse + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function editVariable(StoreOptionVariable $request, $option, $variable) + public function editConfiguration(Request $request, ServiceOption $option) { - $repo = new VariableRepository; - try { - if ($request->input('action') !== 'delete') { - $variable = $repo->update($variable, $request->normalize()); - Alert::success("The service variable '{$variable->name}' has been updated.")->flash(); - } else { - $repo->delete($variable); - Alert::success('That service variable has been deleted.')->flash(); - } - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') - ->flash(); + $this->optionUpdateService->handle($option, $request->all()); + $this->alert->success(trans('admin/services.options.notices.option_updated'))->flash(); + } catch (NoParentConfigurationFoundException $exception) { + dd('hodor'); + $this->alert->danger($exception->getMessage())->flash(); } - return redirect()->route('admin.services.option.variables', $option); + return redirect()->route('admin.services.option.view', $option->id); } /** * Handles POST when updating script for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function updateScripts(EditOptionScript $request) + public function updateScripts(EditOptionScript $request, ServiceOption $option) { try { - $this->repository->scripts($request->normalize()); - - Alert::success('Successfully updated option scripts to be run when servers are installed.')->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') - ->flash(); + $this->installScriptUpdateService->handle($option, $request->normalize()); + $this->alert->success(trans('admin/services.options.notices.script_updated'))->flash(); + } catch (InvalidCopyFromException $exception) { + $this->alert->danger($exception->getMessage())->flash(); } - return redirect()->route('admin.services.option.scripts', $id); + return redirect()->route('admin.services.option.scripts', $option->id); } } diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php new file mode 100644 index 00000000..3d2a4fa4 --- /dev/null +++ b/app/Http/Controllers/Admin/VariableController.php @@ -0,0 +1,147 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Http\Controllers\Admin; + +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; +use Pterodactyl\Services\Services\Variables\VariableCreationService; +use Pterodactyl\Services\Services\Variables\VariableUpdateService; + +class VariableController extends Controller +{ + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServiceVariableRepository + */ + protected $serviceVariableRepository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableUpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + ServiceOptionRepositoryInterface $serviceOptionRepository, + ServiceVariableRepository $serviceVariableRepository, + VariableCreationService $creationService, + VariableUpdateService $updateService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->serviceOptionRepository = $serviceOptionRepository; + $this->serviceVariableRepository = $serviceVariableRepository; + $this->updateService = $updateService; + } + + /** + * Handles POST request to create a new option variable. + * + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function store(OptionVariableFormRequest $request, ServiceOption $option) + { + $this->creationService->handle($option->id, $request->normalize()); + $this->alert->success(trans('admin/services.variables.notices.variable_created'))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } + + /** + * Display variable overview page for a service option. + * + * @param int $option + * @return \Illuminate\View\View + */ + public function view($option) + { + $option = $this->serviceOptionRepository->getWithVariables($option); + + return view('admin.services.options.variables', ['option' => $option]); + } + + /** + * Handles POST when editing a configration for a service variable. + * + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function update(OptionVariableFormRequest $request, ServiceOption $option, ServiceVariable $variable) + { + $this->updateService->handle($variable, $request->normalize()); + $this->alert->success(trans('admin/services.variables.notices.variable_updated', [ + 'variable' => $variable->name, + ]))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } + + /** + * Delete a service variable from the system. + * + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable + * @return \Illuminate\Http\RedirectResponse + */ + public function delete(ServiceOption $option, ServiceVariable $variable) + { + $this->serviceVariableRepository->delete($variable->id); + $this->alert->success(trans('admin/services.variables.notices.variable_deleted', [ + 'variable' => $variable->name, + ]))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } +} diff --git a/app/Http/Requests/Admin/OptionVariableFormRequest.php b/app/Http/Requests/Admin/Service/OptionVariableFormRequest.php similarity index 88% rename from app/Http/Requests/Admin/OptionVariableFormRequest.php rename to app/Http/Requests/Admin/Service/OptionVariableFormRequest.php index fe0a43e5..477e3353 100644 --- a/app/Http/Requests/Admin/OptionVariableFormRequest.php +++ b/app/Http/Requests/Admin/Service/OptionVariableFormRequest.php @@ -50,7 +50,12 @@ class OptionVariableFormRequest extends AdminFormRequest */ public function withValidator($validator) { - $validator->sometimes('default_value', $this->input('rules') ?? null, function ($input) { + $rules = $this->input('rules'); + if ($this->method() === 'PATCH') { + $rules = $this->input('rules', $this->route()->parameter('variable')->rules); + } + + $validator->sometimes('default_value', $rules, function ($input) { return $input->default_value; }); } diff --git a/app/Http/Requests/Admin/ServiceOptionFormRequest.php b/app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php similarity index 86% rename from app/Http/Requests/Admin/ServiceOptionFormRequest.php rename to app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php index 5986e6d7..8867a27f 100644 --- a/app/Http/Requests/Admin/ServiceOptionFormRequest.php +++ b/app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php @@ -22,21 +22,18 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests\Admin; +namespace Pterodactyl\Http\Requests\Admin\Service; use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; class ServiceOptionFormRequest extends AdminFormRequest { /** - * @return array + * {@inheritdoc} */ public function rules() { - if ($this->method() === 'PATCH') { - return ServiceOption::getUpdateRulesForId($this->route()->parameter('option')->id); - } - return ServiceOption::getCreateRules(); } } diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index b62b6eb2..1df3f6aa 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIKey extends Model implements ValidableContract +class APIKey extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/APIPermission.php b/app/Models/APIPermission.php index 626185fc..4072cd56 100644 --- a/app/Models/APIPermission.php +++ b/app/Models/APIPermission.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIPermission extends Model implements ValidableContract +class APIPermission extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 65458be1..cbdba2e1 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Allocation extends Model implements ValidableContract +class Allocation extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Database.php b/app/Models/Database.php index ee8be0c5..4453c790 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Database extends Model implements ValidableContract +class Database extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index d4ec484f..9533c0a5 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class DatabaseHost extends Model implements ValidableContract +class DatabaseHost extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Location.php b/app/Models/Location.php index 19322c6e..0337bcc3 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Location extends Model implements ValidableContract +class Location extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Node.php b/app/Models/Node.php index 3f43a28a..138d29d8 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -29,9 +29,10 @@ use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Node extends Model implements ValidableContract +class Node extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; diff --git a/app/Models/Server.php b/app/Models/Server.php index 98871201..45091372 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -33,9 +33,10 @@ use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Server extends Model implements ValidableContract +class Server extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index 9c0343bd..2b553f49 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceOption extends Model implements ValidableContract +class ServiceOption extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/ServiceVariable.php b/app/Models/ServiceVariable.php index a7838bcd..a341165e 100644 --- a/app/Models/ServiceVariable.php +++ b/app/Models/ServiceVariable.php @@ -27,16 +27,17 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceVariable extends Model implements ValidableContract +class ServiceVariable extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; /** * Reserved environment variable names. * - * @var array + * @var string */ const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; diff --git a/app/Models/User.php b/app/Models/User.php index f95a5242..972d0943 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,6 +32,7 @@ use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Pterodactyl\Exceptions\DisplayException; +use Sofa\Eloquence\Contracts\CleansAttributes; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; use Sofa\Eloquence\Contracts\Validable as ValidableContract; @@ -40,7 +41,12 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, ValidableContract +class User extends Model implements + AuthenticatableContract, + AuthorizableContract, + CanResetPasswordContract, + CleansAttributes, + ValidableContract { use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; @@ -125,6 +131,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Rules verifying that the data passed in forms is valid and meets application logic rules. + * * @var array */ protected static $applicationRules = [ @@ -155,7 +162,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Enables or disables TOTP on an account if the token is valid. * - * @param int $token + * @param int $token * @return bool * @deprecated */ @@ -196,7 +203,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Send the password reset notification. * - * @param string $token + * @param string $token * @return void */ public function sendPasswordResetNotification($token) @@ -218,7 +225,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Returns the user's daemon secret for a given server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return null|string */ public function daemonToken(Server $server) @@ -248,7 +255,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Change the access level for a given call to `access()` on the user. * - * @param string $level can be all, admin, subuser, owner + * @param string $level can be all, admin, subuser, owner * @return $this */ public function setAccessLevel($level = 'all') @@ -265,7 +272,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * Returns an array of all servers a user is able to access. * Note: does not account for user admin status. * - * @param array $load + * @param array $load * @return \Pterodactyl\Models\Server */ public function access(...$load) @@ -303,7 +310,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Store the username as a lowecase string. * - * @param string $value + * @param string $value */ public function setUsernameAttribute($value) { diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 5206501d..8a98e662 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,7 +25,9 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -76,6 +78,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(ServiceOptionRepositoryInterface::class, ServiceOptionRepository::class); + $this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 77108b59..0dd1beee 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -24,8 +24,9 @@ namespace Pterodactyl\Repositories\Eloquent; -use Illuminate\Database\Query\Expression; +use Webmozart\Assert\Assert; use Pterodactyl\Repository\Repository; +use Illuminate\Database\Query\Expression; use Pterodactyl\Contracts\Repository\RepositoryInterface; use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; @@ -48,6 +49,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function create(array $fields, $validate = true, $force = false) { + Assert::boolean($validate, 'Second argument passed to create should be boolean, recieved %s.'); + Assert::boolean($force, 'Third argument passed to create should be boolean, received %s.'); + $instance = $this->getBuilder()->newModelInstance(); if ($force) { @@ -73,6 +77,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function find($id) { + Assert::integer($id, 'First argument passed to find should be integer, received %s.'); + $instance = $this->getBuilder()->find($id, $this->getColumns()); if (! $instance) { @@ -119,6 +125,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function delete($id, $destroy = false) { + Assert::integer($id, 'First argument passed to delete should be integer, received %s.'); + Assert::boolean($destroy, 'Second argument passed to delete should be boolean, received %s.'); + $instance = $this->getBuilder()->where($this->getModel()->getKeyName(), $id); return ($destroy) ? $instance->forceDelete() : $instance->delete(); @@ -129,6 +138,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function deleteWhere(array $attributes, $force = false) { + Assert::boolean($force, 'Second argument passed to deleteWhere should be boolean, received %s.'); + $instance = $this->getBuilder()->where($attributes); return ($force) ? $instance->forceDelete() : $instance->delete(); @@ -139,6 +150,10 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function update($id, array $fields, $validate = true, $force = false) { + Assert::integer($id, 'First argument passed to update expected to be integer, received %s.'); + Assert::boolean($validate, 'Third argument passed to update should be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to update should be boolean, received %s.'); + $instance = $this->getBuilder()->where('id', $id)->first(); if (! $instance) { @@ -167,6 +182,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateWhereIn($column, array $values, array $fields) { + Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn expected to be a string, received %s.'); + return $this->getBuilder()->whereIn($column, $values)->update($fields); } @@ -238,6 +255,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateOrCreate(array $where, array $fields, $validate = true, $force = false) { + Assert::boolean($validate, 'Third argument passed to updateOrCreate should be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to updateOrCreate should be boolean, received %s.'); + $instance = $this->withColumns('id')->findWhere($where)->first(); if (! $instance) { diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index dc05fac0..14869dfb 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -36,4 +36,31 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio { return ServiceOption::class; } + + /** + * {@inheritdoc} + */ + public function getWithVariables($id) + { + return $this->getBuilder()->with('variables')->find($id, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getWithCopyFrom($id) + { + return $this->getBuilder()->with('copyFrom')->find($id, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function isCopiableScript($copyFromId, $service) + { + return $this->getBuilder()->whereNull('copy_script_from') + ->where('id', '=', $copyFromId) + ->where('service_id', '=', $service) + ->exists(); + } } diff --git a/app/Http/Requests/Admin/Service/StoreOptionVariable.php b/app/Repositories/Eloquent/ServiceVariableRepository.php similarity index 65% rename from app/Http/Requests/Admin/Service/StoreOptionVariable.php rename to app/Repositories/Eloquent/ServiceVariableRepository.php index 869b2efc..bf780582 100644 --- a/app/Http/Requests/Admin/Service/StoreOptionVariable.php +++ b/app/Repositories/Eloquent/ServiceVariableRepository.php @@ -22,26 +22,18 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests\Admin\Service; +namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Http\Requests\Admin\AdminFormRequest; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; -class StoreOptionVariable extends AdminFormRequest +class ServiceVariableRepository extends EloquentRepository implements ServiceVariableRepositoryInterface { /** - * Set the rules to be used for data passed to the request. - * - * @return array + * {@inheritdoc} */ - public function rules() + public function model() { - return [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'rules' => 'bail|required|string', - 'default_value' => explode('|', $this->input('rules')), - 'options' => 'sometimes|required|array', - ]; + return ServiceVariable::class; } } diff --git a/app/Repositories/Old/OptionRepository.php b/app/Repositories/Old/OptionRepository.php deleted file mode 100644 index 6e99583c..00000000 --- a/app/Repositories/Old/OptionRepository.php +++ /dev/null @@ -1,241 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Repositories; - -use DB; -use Validator; -use InvalidArgumentException; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class OptionRepository -{ - /** - * Store the requested service option. - * - * @var \Pterodactyl\Models\ServiceOption - */ - protected $model; - - /** - * OptionRepository constructor. - * - * @param null|int|\Pterodactyl\Models\ServiceOption $option - */ - public function __construct($option = null) - { - if (is_null($option)) { - return; - } - - if ($option instanceof ServiceOption) { - $this->model = $option; - } else { - if (! is_numeric($option)) { - throw new InvalidArgumentException( - sprintf('Variable passed to constructor must be integer or instance of \Pterodactyl\Models\ServiceOption.') - ); - } - - $this->model = ServiceOption::findOrFail($option); - } - } - - /** - * Return the eloquent model for the given repository. - * - * @return null|\Pterodactyl\Models\ServiceOption - */ - public function getModel() - { - return $this->model; - } - - /** - * Update the currently assigned model by re-initalizing the class. - * - * @param null|int|\Pterodactyl\Models\ServiceOption $option - * @return $this - */ - public function setModel($option) - { - self::__construct($option); - - return $this; - } - - /** - * Creates a new service option on the system. - * - * @param array $data - * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'service_id' => 'required|numeric|exists:services,id', - 'name' => 'required|string|max:255', - 'description' => 'required|string', - 'tag' => 'required|alpha_num|max:60|unique:service_options,tag', - 'docker_image' => 'sometimes|string|max:255', - 'startup' => 'sometimes|nullable|string', - 'config_from' => 'sometimes|required|numeric|exists:service_options,id', - 'config_startup' => 'required_without:config_from|json', - 'config_stop' => 'required_without:config_from|string|max:255', - 'config_logs' => 'required_without:config_from|json', - 'config_files' => 'required_without:config_from|json', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['config_from'])) { - if (! ServiceOption::where('service_id', $data['service_id'])->where('id', $data['config_from'])->first()) { - throw new DisplayException('The `configuration from` directive must be a child of the assigned service.'); - } - } - - return $this->setModel(ServiceOption::create($data))->getModel(); - } - - /** - * Deletes a service option from the system. - * - * @param int $id - * @return void - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Throwable - */ - public function delete($id) - { - $this->model->load('variables', 'servers'); - - if ($this->model->servers->count() > 0) { - throw new DisplayException('You cannot delete a service option that has servers associated with it.'); - } - - DB::transaction(function () use ($option) { - foreach ($option->variables as $variable) { - (new VariableRepository)->delete($variable->id); - } - - $option->delete(); - }); - } - - /** - * Updates a service option in the database which can then be used - * on nodes. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $option = ServiceOption::findOrFail($id); - - // Due to code limitations (at least when I am writing this currently) - // we have to make an assumption that if config_from is not passed - // that we should be telling it that no config is wanted anymore. - // - // This really is only an issue if we open API access to this function, - // in which case users will always need to pass `config_from` in order - // to keep it assigned. - if (! isset($data['config_from']) && ! is_null($option->config_from)) { - $option->config_from = null; - } - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|max:255', - 'description' => 'sometimes|required|string', - 'tag' => 'sometimes|required|string|max:255|unique:service_options,tag,' . $option->id, - 'docker_image' => 'sometimes|required|string|max:255', - 'startup' => 'sometimes|required|string', - 'config_from' => 'sometimes|required|numeric|exists:service_options,id', - ]); - - $validator->sometimes([ - 'config_startup', 'config_logs', 'config_files', - ], 'required_without:config_from|json', function ($input) use ($option) { - return ! (! $input->config_from && ! is_null($option->config_from)); - }); - - $validator->sometimes('config_stop', 'required_without:config_from|string|max:255', function ($input) use ($option) { - return ! (! $input->config_from && ! is_null($option->config_from)); - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['config_from'])) { - if (! ServiceOption::where('service_id', $option->service_id)->where('id', $data['config_from'])->first()) { - throw new DisplayException('The `configuration from` directive must be a child of the assigned service.'); - } - } - - $option->fill($data)->save(); - - return $option; - } - - /** - * Updates a service option's scripts in the database. - * - * @param array $data - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function scripts(array $data) - { - $data['script_install'] = empty($data['script_install']) ? null : $data['script_install']; - - if (isset($data['copy_script_from']) && ! empty($data['copy_script_from'])) { - $select = ServiceOption::whereNull('copy_script_from') - ->where('id', $data['copy_script_from']) - ->where('service_id', $this->model->service_id) - ->first(); - - if (! $select) { - throw new DisplayException('The service option selected to copy a script from either does not exist, or is copying from a higher level.'); - } - } else { - $data['copy_script_from'] = null; - } - - $this->model->fill($data)->save(); - } -} diff --git a/app/Repositories/Old/VariableRepository.php b/app/Repositories/Old/VariableRepository.php deleted file mode 100644 index 1aded829..00000000 --- a/app/Repositories/Old/VariableRepository.php +++ /dev/null @@ -1,170 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Repositories; - -use DB; -use Validator; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Models\ServiceVariable; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class VariableRepository -{ - /** - * Create a new service variable. - * - * @param int $option - * @param array $data - * @return \Pterodactyl\Models\ServiceVariable - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($option, array $data) - { - $option = ServiceOption::select('id')->findOrFail($option); - - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'sometimes|nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'default_value' => 'string', - 'options' => 'sometimes|required|array', - 'rules' => 'bail|required|string', - ]); - - // Ensure the default value is allowed by the rules provided. - $validator->sometimes('default_value', $data['rules'] ?? null, function ($input) { - return $input->default_value; - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (in_array($data['env_variable'], ServiceVariable::reservedNames())) { - throw new DisplayException('The environment variable name provided is a reserved keyword for the daemon.'); - } - - $search = ServiceVariable::where('env_variable', $data['env_variable'])->where('option_id', $option->id); - if ($search->first()) { - throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.'); - } - - if (! isset($data['options']) || ! is_array($data['options'])) { - $data['options'] = []; - } - - $data['option_id'] = $option->id; - $data['user_viewable'] = (in_array('user_viewable', $data['options'])); - $data['user_editable'] = (in_array('user_editable', $data['options'])); - - // Remove field that isn't used. - unset($data['options']); - - return ServiceVariable::create($data); - } - - /** - * Deletes a specified option variable as well as all server - * variables currently assigned. - * - * @param int $id - * @return void - */ - public function delete($id) - { - $variable = ServiceVariable::with('serverVariable')->findOrFail($id); - - DB::transaction(function () use ($variable) { - foreach ($variable->serverVariable as $v) { - $v->delete(); - } - - $variable->delete(); - }); - } - - /** - * Updates a given service variable. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\ServiceVariable - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $variable = ServiceVariable::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'rules' => 'bail|required|string', - 'options' => 'sometimes|required|array', - ]); - - // Ensure the default value is allowed by the rules provided. - $rules = (isset($data['rules'])) ? $data['rules'] : $variable->rules; - $validator->sometimes('default_value', $rules, function ($input) { - return $input->default_value; - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['env_variable'])) { - if (in_array($data['env_variable'], ServiceVariable::reservedNames())) { - throw new DisplayException('The environment variable name provided is a reserved keyword for the daemon.'); - } - - $search = ServiceVariable::where('env_variable', $data['env_variable']) - ->where('option_id', $variable->option_id) - ->where('id', '!=', $variable->id); - if ($search->first()) { - throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.'); - } - } - - if (! isset($data['options']) || ! is_array($data['options'])) { - $data['options'] = []; - } - - $data['user_viewable'] = (in_array('user_viewable', $data['options'])); - $data['user_editable'] = (in_array('user_editable', $data['options'])); - - // Remove field that isn't used. - unset($data['options']); - - $variable->fill($data)->save(); - - return $variable; - } -} diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php new file mode 100644 index 00000000..f2f13559 --- /dev/null +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -0,0 +1,77 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Services\Options; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; + +class InstallScriptUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * InstallScriptUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository + */ + public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) + { + $this->serviceOptionRepository = $serviceOptionRepository; + } + + /** + * Modify the option install script for a given service option. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException + */ + public function handle($option, array $data) + { + if (! $option instanceof ServiceOption) { + $option = $this->serviceOptionRepository->find($option); + } + + if (! is_null(array_get($data, 'copy_script_from'))) { + if (! $this->serviceOptionRepository->isCopiableScript(array_get($data, 'copy_script_from'), $option->service_id)) { + throw new InvalidCopyFromException(trans('admin/exceptions.service.options.invalid_copy_id')); + } + } + + $this->serviceOptionRepository->withoutFresh()->update($option->id, [ + 'script_install' => array_get($data, 'script_install'), + 'script_is_privileged' => array_get($data, 'script_is_privileged'), + 'script_entry' => array_get($data, 'script_entry'), + 'script_container' => array_get($data, 'script_container'), + 'copy_script_from' => array_get($data, 'copy_script_from'), + ]); + } +} diff --git a/app/Services/Services/Options/CreationService.php b/app/Services/Services/Options/OptionCreationService.php similarity index 87% rename from app/Services/Services/Options/CreationService.php rename to app/Services/Services/Options/OptionCreationService.php index dafc9f8f..83cda6b1 100644 --- a/app/Services/Services/Options/CreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Services\Services\Options; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; -class CreationService +class OptionCreationService { /** * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface @@ -50,8 +50,8 @@ class CreationService * @param array $data * @return \Pterodactyl\Models\ServiceOption * - * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException */ public function handle(array $data) { @@ -62,7 +62,7 @@ class CreationService ]); if ($results !== 1) { - throw new DisplayException(trans('admin/exceptions.service.options.must_be_child')); + throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); } } else { $data['config_from'] = null; diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php new file mode 100644 index 00000000..aa392f35 --- /dev/null +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -0,0 +1,77 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Services\Options; + +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; + +class OptionDeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * OptionDeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + ServiceOptionRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + /** + * Delete an option from the database if it has no active servers attached to it. + * + * @param int $option + * @return int + * + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException + */ + public function handle($option) + { + $servers = $this->serverRepository->findCountWhere([ + ['option_id', '=', $option], + ]); + + if ($servers > 0) { + throw new HasActiveServersException(trans('admin/exceptions.service.options.delete_has_servers')); + } + + return $this->repository->delete($option); + } +} diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php new file mode 100644 index 00000000..4e922b21 --- /dev/null +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -0,0 +1,77 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Services\Options; + +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Models\ServiceOption; + +class OptionUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * OptionUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct(ServiceOptionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Update a service option. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException + */ + public function handle($option, array $data) + { + if (! $option instanceof ServiceOption) { + $option = $this->repository->find($option); + } + + if (! is_null(array_get($data, 'config_from'))) { + $results = $this->repository->findCountWhere([ + ['service_id', '=', $option->service_id], + ['id', '=', array_get($data, 'config_from')], + ]); + + if ($results !== 1) { + throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); + } + } + + $this->repository->withoutFresh()->update($option->id, $data); + } +} diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index db8d9f24..a079baab 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -25,6 +25,10 @@ namespace Pterodactyl\Services\Services\Variables; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; class VariableCreationService { @@ -33,20 +37,45 @@ class VariableCreationService */ protected $serviceOptionRepository; - public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) - { + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + public function __construct( + ServiceOptionRepositoryInterface $serviceOptionRepository, + ServiceVariableRepositoryInterface $serviceVariableRepository + ) { $this->serviceOptionRepository = $serviceOptionRepository; + $this->serviceVariableRepository = $serviceVariableRepository; } /** * Create a new variable for a given service option. * - * @param int $optionId + * @param int $option * @param array $data * @return \Pterodactyl\Models\ServiceVariable + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ - public function handle($optionId, array $data) + public function handle($option, array $data) { - $option = $this->serviceOptionRepository->find($optionId); + if ($option instanceof ServiceOption) { + $option = $option->id; + } + + if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { + throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.')); + } + + $options = array_get($data, 'options', []); + + return $this->serviceVariableRepository->create(array_merge([ + 'option_id' => $option, + 'user_viewable' => in_array('user_viewable', $options), + 'user_editable' => in_array('user_editable', $options), + ], $data)); } } diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php new file mode 100644 index 00000000..c3fb8693 --- /dev/null +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -0,0 +1,88 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Services\Variables; + +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; + +class VariableUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + public function __construct(ServiceVariableRepositoryInterface $serviceVariableRepository) + { + $this->serviceVariableRepository = $serviceVariableRepository; + } + + /** + * Update a specific service variable. + * + * @param int|\Pterodactyl\Models\ServiceVariable $variable + * @param array $data + * @return \Pterodactyl\Models\ServiceVariable + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function handle($variable, array $data) + { + if (! $variable instanceof ServiceVariable) { + $variable = $this->serviceVariableRepository->find($variable); + } + + if (! is_null(array_get($data, 'env_variable'))) { + if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { + throw new ReservedVariableNameException(trans('admin/exceptions.service.variables.reserved_name', [ + 'name' => array_get($data, 'env_variable'), + ])); + } + + $search = $this->serviceVariableRepository->withColumns('id')->findCountWhere([ + ['env_variable', '=', array_get($data, 'env_variable')], + ['option_id', '=', $variable->option_id], + ['id', '!=', $variable->id], + ]); + + if ($search > 0) { + throw new DisplayException(trans('admin/exceptions.service.variables.env_not_unique', [ + 'name' => array_get($data, 'env_variable'), + ])); + } + } + + $options = array_get($data, 'options', []); + + return $this->serviceVariableRepository->update($variable->id, array_merge([ + 'user_viewable' => in_array('user_viewable', $options, $variable->user_viewable), + 'user_editable' => in_array('user_editable', $options, $variable->user_editable), + ], $data)); + } +} diff --git a/composer.json b/composer.json index a39f963c..59e683ec 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "sofa/eloquence": "5.4.1", "spatie/laravel-fractal": "4.0.1", "watson/validating": "3.0.1", + "webmozart/assert": "^1.2", "webpatser/laravel-uuid": "2.0.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 97d0f37c..4f49bd19 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "d4f8198c8d3d27408b5be1a525e8ad4b", + "content-hash": "76f4864c9d8653bb8a6be22a115b7489", "packages": [ { "name": "aws/aws-sdk-php", @@ -3821,6 +3821,56 @@ ], "time": "2016-10-31T21:53:17+00:00" }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23T20:04:58+00:00" + }, { "name": "webpatser/laravel-uuid", "version": "2.0.1", @@ -6027,56 +6077,6 @@ "description": "Symfony Yaml Component", "homepage": "https://symfony.com", "time": "2017-07-23T12:43:26+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23T20:04:58+00:00" } ], "aliases": [], diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 5699bd10..1a575625 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -35,7 +35,13 @@ return [ ], 'service' => [ 'options' => [ - 'must_be_child' => 'The "Configuration From" directive for this option must be a child option for the selected service.', + 'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.', + 'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.', + 'must_be_child' => 'The "Copy Settings From" directive for this option must be a child option for the selected service.', + ], + 'variables' => [ + 'env_not_unique' => 'The environment variable :name must be unique to this service option.', + 'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.', ], ], ]; diff --git a/resources/lang/en/admin/services.php b/resources/lang/en/admin/services.php index fcf59c14..8390140e 100644 --- a/resources/lang/en/admin/services.php +++ b/resources/lang/en/admin/services.php @@ -25,11 +25,15 @@ return [ 'options' => [ 'notices' => [ + 'option_updated' => 'Service option configuration has been updated successfully.', + 'script_updated' => 'Service option install script has been updated and will run whenever servers are installed.', 'option_created' => 'New service option was created successfully. You will need to restart any running daemons to apply this new service.', ], ], 'variables' => [ 'notices' => [ + 'variable_deleted' => 'The variable ":variable" has been deleted and will no longer be available to servers once rebuilt.', + 'variable_updated' => 'The variable ":variable" has been updated. You will need to rebuild any servers using this variable in order to apply changes.', 'variable_created' => 'New variable has successfully been created and assigned to this service option.', ], ], diff --git a/resources/themes/pterodactyl/admin/services/options/variables.blade.php b/resources/themes/pterodactyl/admin/services/options/variables.blade.php index dcc8882c..d0baa8c4 100644 --- a/resources/themes/pterodactyl/admin/services/options/variables.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/variables.blade.php @@ -92,8 +92,8 @@ diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php index 3d302580..24104f79 100644 --- a/resources/themes/pterodactyl/admin/services/options/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -143,10 +143,10 @@ diff --git a/resources/themes/pterodactyl/admin/services/view.blade.php b/resources/themes/pterodactyl/admin/services/view.blade.php index 64cf367c..346a99c8 100644 --- a/resources/themes/pterodactyl/admin/services/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/view.blade.php @@ -84,8 +84,8 @@ diff --git a/routes/admin.php b/routes/admin.php index b205199a..229cf188 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -170,19 +170,21 @@ Route::group(['prefix' => 'services'], function () { Route::get('/view/{id}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new'); Route::get('/option/{option}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); - Route::get('/option/{option}/variables', 'OptionController@viewVariables')->name('admin.services.option.variables'); + Route::get('/option/{option}/variables', 'VariableController@view')->name('admin.services.option.variables'); Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); Route::post('/new', 'ServiceController@store'); - Route::post('/view/{option}', 'ServiceController@edit'); Route::post('/option/new', 'OptionController@store'); - Route::post('/option/{option}/scripts', 'OptionController@updateScripts'); - Route::post('/option/{option}/variables', 'OptionController@createVariable'); - Route::post('/option/{option}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit'); + Route::post('/option/{option}/variables', 'VariableController@store'); + Route::patch('/view/{option}', 'ServiceController@edit'); Route::patch('/option/{option}', 'OptionController@editConfiguration'); + Route::patch('/option/{option}/scripts', 'OptionController@updateScripts'); + Route::patch('/option/{option}/variables/{variable}', 'VariableController@update')->name('admin.services.option.variables.edit'); Route::delete('/view/{id}', 'ServiceController@delete'); + Route::delete('/option/{option}', 'OptionController@delete'); + Route::delete('/option/{option}/variables/{variable}', 'VariableController@delete'); }); /*