From cdfbc600304216164200659e550700c48d098f64 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 20 Aug 2017 19:23:50 -0500 Subject: [PATCH] Push pack services and fix for failing tests --- .../Repository/PackRepositoryInterface.php | 20 ++ .../Service/HasActiveServersException.php | 4 +- .../Pack/ZipArchiveCreationException.php | 30 +++ .../Controllers/Admin/NodesController.php | 9 +- .../Controllers/Admin/OptionController.php | 27 +- app/Http/Controllers/Admin/PackController.php | 194 +++++++------- .../Controllers/Admin/ServiceController.php | 13 +- app/Http/Requests/Admin/PackFormRequest.php | 64 +++++ app/Providers/RepositoryServiceProvider.php | 3 + app/Repositories/Eloquent/PackRepository.php | 36 +++ app/Repositories/Old/PackRepository.php | 239 ------------------ app/Services/Nodes/DeletionService.php | 6 +- app/Services/Packs/ExportPackService.php | 112 ++++++++ config/pterodactyl.php | 1 + database/seeds/RustServiceTableSeeder.php | 5 +- database/seeds/SourceServiceTableSeeder.php | 5 +- database/seeds/TerrariaServiceTableSeeder.php | 5 +- database/seeds/VoiceServiceTableSeeder.php | 5 +- resources/lang/en/admin/pack.php | 2 + .../pterodactyl/admin/packs/view.blade.php | 4 +- routes/admin.php | 9 +- 21 files changed, 415 insertions(+), 378 deletions(-) create mode 100644 app/Exceptions/Service/Pack/ZipArchiveCreationException.php create mode 100644 app/Http/Requests/Admin/PackFormRequest.php delete mode 100644 app/Repositories/Old/PackRepository.php create mode 100644 app/Services/Packs/ExportPackService.php diff --git a/app/Contracts/Repository/PackRepositoryInterface.php b/app/Contracts/Repository/PackRepositoryInterface.php index 5d1bb022e..5160245c0 100644 --- a/app/Contracts/Repository/PackRepositoryInterface.php +++ b/app/Contracts/Repository/PackRepositoryInterface.php @@ -28,12 +28,32 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface PackRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Return a paginated listing of packs with their associated option and server count. + * + * @param int $paginate + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginateWithOptionAndServerCount($paginate = 50); + + /** + * Return a pack with the associated server models attached to it. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function getWithServers($id); + /** * Return all of the file archives for a given pack. * * @param int $id * @param bool $collection * @return object|\Illuminate\Support\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function getFileArchives($id, $collection = false); } diff --git a/app/Exceptions/Service/HasActiveServersException.php b/app/Exceptions/Service/HasActiveServersException.php index 851052d04..09c13c186 100644 --- a/app/Exceptions/Service/HasActiveServersException.php +++ b/app/Exceptions/Service/HasActiveServersException.php @@ -24,7 +24,9 @@ namespace Pterodactyl\Exceptions\Service; -class HasActiveServersException extends \Exception +use Pterodactyl\Exceptions\DisplayException; + +class HasActiveServersException extends DisplayException { // } diff --git a/app/Exceptions/Service/Pack/ZipArchiveCreationException.php b/app/Exceptions/Service/Pack/ZipArchiveCreationException.php new file mode 100644 index 000000000..14eb8ce9b --- /dev/null +++ b/app/Exceptions/Service/Pack/ZipArchiveCreationException.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\Service\Pack; + +class ZipArchiveCreationException extends \Exception +{ + // +} diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 4be09917a..854ab486a 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -174,6 +174,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewIndex($node) { @@ -213,6 +215,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewAllocation($node) { @@ -227,6 +231,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewServers($node) { @@ -299,9 +305,10 @@ class NodesController extends Controller * Sets an alias for a specific allocation on a node. * * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response + * @return \Symfony\Component\HttpFoundation\Response * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function allocationSetAlias(AllocationAliasFormRequest $request) { diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index d07897993..04e69e850 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -30,7 +30,6 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; -use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionUpdateService; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Services\Services\Options\OptionCreationService; @@ -81,13 +80,13 @@ class OptionController extends Controller /** * OptionController constructor. * - * @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 + * @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, @@ -147,17 +146,13 @@ class OptionController extends Controller * * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function destroy(ServiceOption $option) { - try { - $this->optionDeletionService->handle($option->id); - $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); - } catch (HasActiveServersException $exception) { - $this->alert->danger($exception->getMessage())->flash(); - - return redirect()->route('admin.services.option.view', $option->id); - } + $this->optionDeletionService->handle($option->id); + $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); return redirect()->route('admin.services.view', $option->service_id); } diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index 9a1b089f3..ab87807ca 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -24,22 +24,19 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\PackFormRequest; +use Pterodactyl\Services\Packs\ExportPackService; use Pterodactyl\Services\Packs\PackCreationService; use Pterodactyl\Services\Packs\PackDeletionService; use Pterodactyl\Services\Packs\PackUpdateService; use Pterodactyl\Services\Packs\TemplateUploadService; -use Storage; use Illuminate\Http\Request; use Pterodactyl\Models\Pack; -use Pterodactyl\Models\Service; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\PackRepository; -use Pterodactyl\Exceptions\DisplayValidationException; class PackController extends Controller { @@ -48,6 +45,11 @@ class PackController extends Controller */ protected $alert; + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + /** * @var \Pterodactyl\Services\Packs\PackCreationService */ @@ -58,6 +60,11 @@ class PackController extends Controller */ protected $deletionService; + /** + * @var \Pterodactyl\Services\Packs\ExportPackService + */ + protected $exportService; + /** * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface */ @@ -66,7 +73,12 @@ class PackController extends Controller /** * @var \Pterodactyl\Services\Packs\PackUpdateService */ - protected $packUpdateService; + protected $updateService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; /** * @var \Pterodactyl\Services\Packs\TemplateUploadService @@ -76,26 +88,35 @@ class PackController extends Controller /** * PackController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\Packs\PackCreationService $creationService - * @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService - * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository - * @param \Pterodactyl\Services\Packs\PackUpdateService $packUpdateService - * @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Services\Packs\ExportPackService $exportService + * @param \Pterodactyl\Services\Packs\PackCreationService $creationService + * @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \Pterodactyl\Services\Packs\PackUpdateService $updateService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService */ public function __construct( AlertsMessageBag $alert, + ConfigRepository $config, + ExportPackService $exportService, PackCreationService $creationService, PackDeletionService $deletionService, PackRepositoryInterface $repository, - PackUpdateService $packUpdateService, + PackUpdateService $updateService, + ServiceRepositoryInterface $serviceRepository, TemplateUploadService $templateUploadService ) { $this->alert = $alert; + $this->config = $config; $this->creationService = $creationService; $this->deletionService = $deletionService; + $this->exportService = $exportService; $this->repository = $repository; - $this->packUpdateService = $packUpdateService; + $this->updateService = $updateService; + $this->serviceRepository = $serviceRepository; $this->templateUploadService = $templateUploadService; } @@ -107,45 +128,41 @@ class PackController extends Controller */ public function index(Request $request) { - $packs = Pack::with('option')->withCount('servers'); - - if (! is_null($request->input('query'))) { - $packs->search($request->input('query')); - } - - return view('admin.packs.index', ['packs' => $packs->paginate(50)]); + return view('admin.packs.index', [ + 'packs' => $this->repository->search($request->input('query'))->paginateWithOptionAndServerCount( + $this->config->get('pterodactyl.paginate.admin.packs') + ), + ]); } /** * Display new pack creation form. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('admin.packs.new', [ - 'services' => Service::with('options')->get(), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Display new pack creation modal for use with template upload. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function newTemplate(Request $request) + public function newTemplate() { return view('admin.packs.modal', [ - 'services' => Service::with('options')->get(), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Handle create pack request and route user to location. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -155,12 +172,12 @@ class PackController extends Controller * @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException * @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException */ - public function store(Request $request) + public function store(PackFormRequest $request) { if ($request->has('from_template')) { - $pack = $this->templateUploadService->handle($request->input('option_id'), $request->input('file_upload')); + $pack = $this->templateUploadService->handle($request->input('option_id'), $request->file('file_upload')); } else { - $pack = $this->creationService->handle($request->normalize(), $request->input('file_upload')); + $pack = $this->creationService->handle($request->normalize(), $request->file('file_upload')); } $this->alert->success(trans('admin/pack.notices.pack_created'))->flash(); @@ -171,98 +188,75 @@ class PackController extends Controller /** * Display pack view template to user. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $pack * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view($pack) { return view('admin.packs.view', [ - 'pack' => Pack::with('servers.node', 'servers.user')->findOrFail($id), - 'services' => Service::with('options')->get(), + 'pack' => $this->repository->getWithServers($pack), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Handle updating or deleting pack information. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request + * @param \Pterodactyl\Models\Pack $pack * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function update(Request $request, $id) + public function update(PackFormRequest $request, Pack $pack) { - $repo = new PackRepository; + $this->updateService->handle($pack, $request->normalize()); + $this->alert->success(trans('admin/pack.notices.pack_updated'))->flash(); - try { - if ($request->input('action') !== 'delete') { - $pack = $repo->update($id, $request->intersect([ - 'name', 'description', 'version', - 'option_id', 'selectable', 'visible', 'locked', - ])); - Alert::success('Pack successfully updated.')->flash(); - } else { - $repo->delete($id); - Alert::success('Pack was successfully deleted from the system.')->flash(); + return redirect()->route('admin.packs.view', $pack->id); + } - return redirect()->route('admin.packs'); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.packs.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to edit this service pack. This error has been logged.')->flash(); - } + /** + * Delete a pack if no servers are attached to it currently. + * + * @param \Pterodactyl\Models\Pack $pack + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + */ + public function destroy(Pack $pack) + { + $this->deletionService->handle($pack->id); + $this->alert->success(trans('admin/pack.notices.pack_deleted', [ + 'name' => $pack->name, + ]))->flash(); - return redirect()->route('admin.packs.view', $id); + return redirect()->route('admin.packs'); } /** * Creates an archive of the pack and downloads it to the browser. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @param bool $files - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + * @param \Pterodactyl\Models\Pack $pack + * @param bool|string $files + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException */ - public function export(Request $request, $id, $files = false) + public function export(Pack $pack, $files = false) { - $pack = Pack::findOrFail($id); - $json = [ - 'name' => $pack->name, - 'version' => $pack->version, - 'description' => $pack->description, - 'selectable' => $pack->selectable, - 'visible' => $pack->visible, - 'locked' => $pack->locked, - ]; - - $filename = tempnam(sys_get_temp_dir(), 'pterodactyl_'); - if ($files === 'with-files') { - $zip = new \ZipArchive; - if (! $zip->open($filename, \ZipArchive::CREATE)) { - abort(503, 'Unable to open file for writing.'); - } - - $files = Storage::files('packs/' . $pack->uuid); - foreach ($files as $file) { - $zip->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file))); - } - - $zip->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT)); - $zip->close(); + $filename = $this->exportService->handle($pack, is_string($files)); + if (is_string($files)) { return response()->download($filename, 'pack-' . $pack->name . '.zip')->deleteFileAfterSend(true); - } else { - $fp = fopen($filename, 'a+'); - fwrite($fp, json_encode($json, JSON_PRETTY_PRINT)); - fclose($fp); - - return response()->download($filename, 'pack-' . $pack->name . '.json', [ - 'Content-Type' => 'application/json', - ])->deleteFileAfterSend(true); } + + return response()->download($filename, 'pack-' . $pack->name . '.json', [ + 'Content-Type' => 'application/json', + ])->deleteFileAfterSend(true); } } diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index b2341bf5b..26f6c0ce1 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -30,7 +30,6 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Services\ServiceUpdateService; use Pterodactyl\Services\Services\ServiceCreationService; use Pterodactyl\Services\Services\ServiceDeletionService; -use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest; @@ -179,17 +178,13 @@ class ServiceController extends Controller * * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function destroy(Service $service) { - try { - $this->deletionService->handle($service->id); - $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); - } catch (HasActiveServersException $exception) { - $this->alert->danger($exception->getMessage())->flash(); - - return redirect()->back(); - } + $this->deletionService->handle($service->id); + $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); return redirect()->route('admin.services'); } diff --git a/app/Http/Requests/Admin/PackFormRequest.php b/app/Http/Requests/Admin/PackFormRequest.php new file mode 100644 index 000000000..75fd9063e --- /dev/null +++ b/app/Http/Requests/Admin/PackFormRequest.php @@ -0,0 +1,64 @@ +. + * + * 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\Requests\Admin; + +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackCreationService; + +class PackFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return Pack::getUpdateRulesForId($this->route()->parameter('pack')->id); + } + + return Pack::getCreateRules(); + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + if ($this->method() !== 'POST') { + return; + } + + $validator->after(function ($validator) { + $mimetypes = implode(',', PackCreationService::VALID_UPLOAD_TYPES); + + /* @var $validator \Illuminate\Validation\Validator */ + $validator->sometimes('file_upload', 'sometimes|required|file|mimetypes:' . $mimetypes, function () { + return true; + }); + }); + } +} diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 0dd26a4bf..b1be571ea 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\PackRepositoryInterface; use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\PackRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -74,6 +76,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); $this->app->bind(NodeRepositoryInterface::class, NodeRepository::class); $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); + $this->app->bind(PackRepositoryInterface::class, PackRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); diff --git a/app/Repositories/Eloquent/PackRepository.php b/app/Repositories/Eloquent/PackRepository.php index 38a824715..5f1641f78 100644 --- a/app/Repositories/Eloquent/PackRepository.php +++ b/app/Repositories/Eloquent/PackRepository.php @@ -24,10 +24,12 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Models\Pack; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Webmozart\Assert\Assert; class PackRepository extends EloquentRepository implements PackRepositoryInterface { @@ -46,7 +48,14 @@ class PackRepository extends EloquentRepository implements PackRepositoryInterfa */ public function getFileArchives($id, $collection = false) { + Assert::numeric($id, 'First argument passed to getFileArchives must be numeric, received %s.'); + Assert::boolean($collection, 'Second argument passed to getFileArchives must be boolean, received %s.'); + $pack = $this->getBuilder()->find($id, ['id', 'uuid']); + if (! $pack) { + throw new ModelNotFoundException; + } + $storage = $this->app->make(FilesystemFactory::class); $files = collect($storage->disk('default')->files('packs/' . $pack->uuid)); @@ -62,4 +71,31 @@ class PackRepository extends EloquentRepository implements PackRepositoryInterfa return ($collection) ? $files : (object) $files->all(); } + + /** + * {@inheritdoc} + */ + public function getWithServers($id) + { + Assert::numeric($id, 'First argument passed to getWithServers must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('servers.node', 'servers.user')->find($id, $this->getColumns()); + if (! $instance) { + throw new ModelNotFoundException; + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function paginateWithOptionAndServerCount($paginate = 50) + { + Assert::integer($paginate, 'First argument passed to paginateWithOptionAndServerCount must be integer, received %s.'); + + return $this->getBuilder()->with('option')->withCount('servers') + ->search($this->searchTerm) + ->paginate($paginate, $this->getColumns()); + } } diff --git a/app/Repositories/Old/PackRepository.php b/app/Repositories/Old/PackRepository.php deleted file mode 100644 index 0a8854465..000000000 --- a/app/Repositories/Old/PackRepository.php +++ /dev/null @@ -1,239 +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 Uuid; -use Storage; -use Validator; -use Pterodactyl\Models\Pack; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class PackRepository -{ - /** - * Creates a new pack on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'name' => 'required|string', - 'version' => 'required|string', - 'description' => 'sometimes|nullable|string', - 'selectable' => 'sometimes|required|boolean', - 'visible' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - 'option_id' => 'required|exists:service_options,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['file_upload'])) { - if (! $data['file_upload']->isValid()) { - throw new DisplayException('The file provided does not appear to be valid.'); - } - - if (! in_array($data['file_upload']->getMimeType(), ['application/gzip', 'application/x-gzip'])) { - throw new DisplayException('The file provided (' . $data['file_upload']->getMimeType() . ') does not meet the required filetype of application/gzip.'); - } - } - - return DB::transaction(function () use ($data) { - $uuid = new UuidService(); - - $pack = new Pack; - $pack->uuid = $uuid->generate('packs', 'uuid'); - $pack->fill([ - 'option_id' => $data['option_id'], - 'name' => $data['name'], - 'version' => $data['version'], - 'description' => (empty($data['description'])) ? null : $data['description'], - 'selectable' => isset($data['selectable']), - 'visible' => isset($data['visible']), - 'locked' => isset($data['locked']), - ])->save(); - - if (! $pack->exists) { - throw new DisplayException('Model does not exist after creation. Did an event prevent it from saving?'); - } - - Storage::makeDirectory('packs/' . $pack->uuid); - if (isset($data['file_upload'])) { - $data['file_upload']->storeAs('packs/' . $pack->uuid, 'archive.tar.gz'); - } - - return $pack; - }); - } - - /** - * Creates a new pack on the system given a template file. - * - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function createWithTemplate(array $data) - { - if (! isset($data['file_upload'])) { - throw new DisplayException('No template file was found submitted with this request.'); - } - - if (! $data['file_upload']->isValid()) { - throw new DisplayException('The file provided does not appear to be valid.'); - } - - if (! in_array($data['file_upload']->getMimeType(), [ - 'application/zip', - 'text/plain', - 'application/json', - ])) { - throw new DisplayException('The file provided (' . $data['file_upload']->getMimeType() . ') does not meet the required filetypes of application/zip or application/json.'); - } - - if ($data['file_upload']->getMimeType() === 'application/zip') { - $zip = new \ZipArchive; - if (! $zip->open($data['file_upload']->path())) { - throw new DisplayException('The uploaded archive was unable to be opened.'); - } - - $isTar = $zip->locateName('archive.tar.gz'); - - if (! $zip->locateName('import.json') || ! $isTar) { - throw new DisplayException('This contents of the provided archive were in an invalid format.'); - } - - $json = json_decode($zip->getFromName('import.json')); - $pack = $this->create([ - 'name' => $json->name, - 'version' => $json->version, - 'description' => $json->description, - 'option_id' => $data['option_id'], - 'selectable' => $json->selectable, - 'visible' => $json->visible, - 'locked' => $json->locked, - ]); - - if (! $zip->extractTo(storage_path('app/packs/' . $pack->uuid), 'archive.tar.gz')) { - $pack->delete(); - throw new DisplayException('Unable to extract the archive file to the correct location.'); - } - - $zip->close(); - - return $pack; - } else { - $json = json_decode(file_get_contents($data['file_upload']->path())); - - return $this->create([ - 'name' => $json->name, - 'version' => $json->version, - 'description' => $json->description, - 'option_id' => $data['option_id'], - 'selectable' => $json->selectable, - 'visible' => $json->visible, - 'locked' => $json->locked, - ]); - } - } - - /** - * Updates a pack on the system. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string', - 'option_id' => 'sometimes|required|exists:service_options,id', - 'version' => 'sometimes|required|string', - 'description' => 'sometimes|string', - 'selectable' => 'sometimes|required|boolean', - 'visible' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $pack = Pack::withCount('servers')->findOrFail($id); - - if ($pack->servers_count > 0 && (isset($data['option_id']) && (int) $data['option_id'] !== $pack->option_id)) { - throw new DisplayException('You cannot modify the associated option if servers are attached to a pack.'); - } - - $pack->fill([ - 'name' => isset($data['name']) ? $data['name'] : $pack->name, - 'option_id' => isset($data['option_id']) ? $data['option_id'] : $pack->option_id, - 'version' => isset($data['version']) ? $data['version'] : $pack->version, - 'description' => (empty($data['description'])) ? null : $data['description'], - 'selectable' => isset($data['selectable']), - 'visible' => isset($data['visible']), - 'locked' => isset($data['locked']), - ])->save(); - - return $pack; - } - - /** - * Deletes a pack and files from the system. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $pack = Pack::withCount('servers')->findOrFail($id); - - if ($pack->servers_count > 0) { - throw new DisplayException('Cannot delete a pack from the system if servers are assocaited with it.'); - } - - DB::transaction(function () use ($pack) { - $pack->delete(); - Storage::deleteDirectory('packs/' . $pack->uuid); - }); - } -} diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php index 519aed42c..6cbab2644 100644 --- a/app/Services/Nodes/DeletionService.php +++ b/app/Services/Nodes/DeletionService.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Services\Nodes; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Models\Node; -use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -70,7 +70,7 @@ class DeletionService * @param int|\Pterodactyl\Models\Node $node * @return bool|null * - * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function handle($node) { @@ -80,7 +80,7 @@ class DeletionService $servers = $this->serverRepository->withColumns('id')->findCountWhere([['node_id', '=', $node]]); if ($servers > 0) { - throw new DisplayException($this->translator->trans('admin/exceptions.node.servers_attached')); + throw new HasActiveServersException($this->translator->trans('admin/exceptions.node.servers_attached')); } return $this->repository->delete($node); diff --git a/app/Services/Packs/ExportPackService.php b/app/Services/Packs/ExportPackService.php new file mode 100644 index 000000000..1432efb9b --- /dev/null +++ b/app/Services/Packs/ExportPackService.php @@ -0,0 +1,112 @@ +. + * + * 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\Packs; + +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException; +use Pterodactyl\Models\Pack; +use ZipArchive; + +class ExportPackService +{ + /** + * @var \ZipArchive + */ + protected $archive; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * ExportPackService constructor. + * + * @param \Illuminate\Contracts\Filesystem\Factory $storage + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \ZipArchive $archive + */ + public function __construct( + FilesystemFactory $storage, + PackRepositoryInterface $repository, + ZipArchive $archive + ) { + $this->archive = $archive; + $this->repository = $repository; + $this->storage = $storage; + } + + /** + * Prepare a pack for export. + * + * @param int|\Pterodactyl\Models\Pack $pack + * @param bool $files + * @return string + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException + */ + public function handle($pack, $files = false) + { + if (! $pack instanceof Pack) { + $pack = $this->repository->find($pack); + } + + $json = [ + 'name' => $pack->name, + 'version' => $pack->version, + 'description' => $pack->description, + 'selectable' => $pack->selectable, + 'visible' => $pack->visible, + 'locked' => $pack->locked, + ]; + + $filename = tempnam(sys_get_temp_dir(), 'pterodactyl_'); + if ($files) { + if (! $this->archive->open($filename, $this->archive::CREATE)) { + throw new ZipArchiveCreationException; + } + + foreach ($this->storage->disk()->files('packs/' . $pack->uuid) as $file) { + $this->archive->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file))); + } + + $this->archive->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT)); + $this->archive->close(); + } else { + $fp = fopen($filename, 'a+'); + fwrite($fp, json_encode($json, JSON_PRETTY_PRINT)); + fclose($fp); + } + + return $filename; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 6e7767db9..ba604d1bc 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -42,6 +42,7 @@ return [ 'admin' => [ 'servers' => env('APP_PAGINATE_ADMIN_SERVERS', 25), 'users' => env('APP_PAGINATE_ADMIN_USERS', 25), + 'packs' => env('APP_PAGINATE_ADMIN_PACKS', 50), ], 'api' => [ 'nodes' => env('APP_PAGINATE_API_NODES', 25), diff --git a/database/seeds/RustServiceTableSeeder.php b/database/seeds/RustServiceTableSeeder.php index 681d97178..e6688ef87 100644 --- a/database/seeds/RustServiceTableSeeder.php +++ b/database/seeds/RustServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class RustServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class RustServiceTableSeeder extends Seeder 'name' => 'Rust', 'description' => 'The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.', 'startup' => './RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity "rust" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \"{{HOSTNAME}}\" +server.level \"{{LEVEL}}\" +server.description \"{{DESCRIPTION}}\" +server.url \"{{URL}}\" +server.headerimage \"{{SERVER_IMG}}\" +server.worldsize \"{{WORLD_SIZE}}\" +server.seed \"{{SEED}}\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \"{{RCON_PASS}}\" {{ADDITIONAL_ARGS}}', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/SourceServiceTableSeeder.php b/database/seeds/SourceServiceTableSeeder.php index f41d1a877..a20ce2552 100644 --- a/database/seeds/SourceServiceTableSeeder.php +++ b/database/seeds/SourceServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class SourceServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class SourceServiceTableSeeder extends Seeder 'name' => 'Source Engine', 'description' => 'Includes support for most Source Dedicated Server games.', 'startup' => './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +ip 0.0.0.0 -strictportbind -norestart', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/TerrariaServiceTableSeeder.php b/database/seeds/TerrariaServiceTableSeeder.php index 6d451f12b..72afdd86f 100644 --- a/database/seeds/TerrariaServiceTableSeeder.php +++ b/database/seeds/TerrariaServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class TerrariaServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class TerrariaServiceTableSeeder extends Seeder 'name' => 'Terraria', 'description' => 'Terraria is a land of adventure! A land of mystery! A land that\'s yours to shape, defend, and enjoy. Your options in Terraria are limitless. Are you an action gamer with an itchy trigger finger? A master builder? A collector? An explorer? There\'s something for everyone.', 'startup' => 'mono TerrariaServer.exe -port {{SERVER_PORT}} -autocreate 2 -worldname World', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/VoiceServiceTableSeeder.php b/database/seeds/VoiceServiceTableSeeder.php index 1b3a05548..cd0ba033e 100644 --- a/database/seeds/VoiceServiceTableSeeder.php +++ b/database/seeds/VoiceServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class VoiceServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class VoiceServiceTableSeeder extends Seeder 'name' => 'Voice Servers', 'description' => 'Voice servers such as Mumble and Teamspeak 3.', 'startup' => '', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/resources/lang/en/admin/pack.php b/resources/lang/en/admin/pack.php index 6e51903a3..d41cfd4d5 100644 --- a/resources/lang/en/admin/pack.php +++ b/resources/lang/en/admin/pack.php @@ -24,6 +24,8 @@ return [ 'notices' => [ + 'pack_updated' => 'Pack has been successfully updated.', + 'pack_deleted' => 'Successfully deleted the pack ":name" from the system.', 'pack_created' => 'A new pack was successfully created on the system and is now available for deployment to servers.', ], ]; diff --git a/resources/themes/pterodactyl/admin/packs/view.blade.php b/resources/themes/pterodactyl/admin/packs/view.blade.php index 03b331bee..44f656864 100644 --- a/resources/themes/pterodactyl/admin/packs/view.blade.php +++ b/resources/themes/pterodactyl/admin/packs/view.blade.php @@ -107,8 +107,8 @@ diff --git a/routes/admin.php b/routes/admin.php index 664ccf528..269802542 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -200,9 +200,12 @@ Route::group(['prefix' => 'packs'], function () { Route::get('/', 'PackController@index')->name('admin.packs'); Route::get('/new', 'PackController@create')->name('admin.packs.new'); Route::get('/new/template', 'PackController@newTemplate')->name('admin.packs.new.template'); - Route::get('/view/{id}', 'PackController@view')->name('admin.packs.view'); + Route::get('/view/{pack}', 'PackController@view')->name('admin.packs.view'); Route::post('/new', 'PackController@store'); - Route::post('/view/{id}', 'PackController@update'); - Route::post('/view/{id}/export/{files?}', 'PackController@export')->name('admin.packs.view.export'); + Route::post('/view/{pack}/export/{files?}', 'PackController@export')->name('admin.packs.view.export'); + + Route::patch('/view/{pack}', 'PackController@update'); + + Route::delete('/view/{pack}', 'PackController@destroy'); });