From 220789a4b971aaaf362d0ba53a3c57e366324d1a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Oct 2017 20:54:22 -0500 Subject: [PATCH 01/34] Push migrations to change existing service structure --- ...angeServicesToUseAMoreUniqueIdentifier.php | 50 ++++++++++++++++++ ...ngeToABetterUniqueServiceConfiguration.php | 51 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php create mode 100644 database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php diff --git a/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php new file mode 100644 index 000000000..64d31f749 --- /dev/null +++ b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php @@ -0,0 +1,50 @@ +dropUnique(['name']); + $table->dropUnique(['file']); + + $table->string('author')->change(); + $table->char('uuid', 36)->after('id'); + $table->dropColumn('folder'); + }); + + DB::table('services')->get(['id', 'author', 'uuid'])->each(function ($service) { + DB::table('services')->where('id', $service->id)->update([ + 'author' => ($service->author === 'ptrdctyl-v040-11e6-8b77-86f30ca893d3') ? 'support@pterodactyl.io' : 'unknown@unknown-author.com', + 'uuid' => Uuid::uuid4()->toString(), + ]); + }); + + Schema::table('services', function (Blueprint $table) { + $table->unique('uuid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('uuid'); + $table->string('folder')->unique('file'); + $table->char('author', 36)->change(); + + $table->unique('name'); + }); + } +} diff --git a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php new file mode 100644 index 000000000..c8159826a --- /dev/null +++ b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php @@ -0,0 +1,51 @@ +char('uuid', 36)->after('id'); + $table->string('author')->after('service_id'); + + $table->index(['service_id', 'tag']); + }); + + DB::table('service_options')->select([ + 'service_options.id', + 'service_options.author', + 'service_options.uuid', + 'services.author AS service_author', + ])->join('services', 'services.id', '=', 'service_options.service_id')->get()->each(function ($option) { + DB::table('service_options')->where('id', $option->id)->update([ + 'author' => $option->service_author, + 'uuid' => Uuid::uuid4()->toString(), + ]); + }); + + Schema::table('service_options', function (Blueprint $table) { + $table->unique('uuid'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('service_options', function (Blueprint $table) { + $table->dropColumn('uuid'); + $table->dropColumn('author'); + $table->dropIndex(['service_id', 'tag']); + }); + } +} From 493c5888a3c80b38231cf1c3a4f03f42de6d786b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Oct 2017 22:03:01 -0500 Subject: [PATCH 02/34] Migration change --- ...ngeToABetterUniqueServiceConfiguration.php | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php index c8159826a..990a07a63 100644 --- a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php +++ b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php @@ -15,21 +15,22 @@ class ChangeToABetterUniqueServiceConfiguration extends Migration { Schema::table('service_options', function (Blueprint $table) { $table->char('uuid', 36)->after('id'); - $table->string('author')->after('service_id'); $table->index(['service_id', 'tag']); }); - DB::table('service_options')->select([ - 'service_options.id', - 'service_options.author', - 'service_options.uuid', - 'services.author AS service_author', - ])->join('services', 'services.id', '=', 'service_options.service_id')->get()->each(function ($option) { - DB::table('service_options')->where('id', $option->id)->update([ - 'author' => $option->service_author, - 'uuid' => Uuid::uuid4()->toString(), - ]); + DB::transaction(function () { + DB::table('service_options')->select([ + 'service_options.id', + 'service_options.uuid', + 'service_options.tag', + 'services.author AS service_author', + ])->join('services', 'services.id', '=', 'service_options.service_id')->get()->each(function ($option) { + DB::table('service_options')->where('id', $option->id)->update([ + 'tag' => $option->service_author . ':' . $option->tag, + 'uuid' => Uuid::uuid4()->toString(), + ]); + }); }); Schema::table('service_options', function (Blueprint $table) { @@ -44,8 +45,15 @@ class ChangeToABetterUniqueServiceConfiguration extends Migration { Schema::table('service_options', function (Blueprint $table) { $table->dropColumn('uuid'); - $table->dropColumn('author'); $table->dropIndex(['service_id', 'tag']); }); + + DB::transaction(function () { + DB::table('service_options')->select(['id', 'author'])->get()->each(function ($option) { + DB::table('service_options')->where('id', $option->id)->update([ + 'tag' => array_get(explode(':', $option->tag), 1), + ]); + }); + }); } } From ae671e6b19083943f31aa55353c4c84b140d3c4b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Oct 2017 22:51:13 -0500 Subject: [PATCH 03/34] Begin updating UI --- .env.example | 4 +-- .../Environment/AppSettingsCommand.php | 9 ++--- .../Repository/ServiceRepositoryInterface.php | 15 ++++++-- .../Controllers/Admin/ServiceController.php | 29 ++++++++++----- app/Http/Requests/Admin/AdminFormRequest.php | 11 ++++-- .../Admin/Service/ServiceFormRequest.php | 14 ++------ app/Models/Service.php | 18 +++++----- app/Models/ServiceOption.php | 27 +++++++++++--- .../Eloquent/ServiceRepository.php | 36 +++++++++++++------ .../Options/InstallScriptUpdateService.php | 2 +- .../Options/OptionCreationService.php | 24 +++++++++++-- .../Options/OptionDeletionService.php | 5 +-- .../Services/Options/OptionUpdateService.php | 2 +- .../Services/ServiceCreationService.php | 11 +++--- .../Services/ServiceDeletionService.php | 2 +- .../Services/ServiceUpdateService.php | 2 +- .../admin/services/index.blade.php | 9 ++++- .../pterodactyl/admin/services/new.blade.php | 7 ---- .../admin/services/options/new.blade.php | 13 ++++--- .../admin/services/options/view.blade.php | 11 +++--- .../pterodactyl/admin/services/view.blade.php | 27 ++++++++------ .../pterodactyl/layouts/admin.blade.php | 6 ++++ 22 files changed, 182 insertions(+), 102 deletions(-) diff --git a/.env.example b/.env.example index 45644374d..4bcc8f12c 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,4 @@ QUEUE_HIGH=high QUEUE_STANDARD=standard QUEUE_LOW=low -SQS_KEY=aws-public -SQS_SECRET=aws-secret -SQS_QUEUE_PREFIX=aws-queue-prefix +SERVICE_AUTHOR=undefined@unknown-author.com diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index d3e63af37..a7d602b5f 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -9,7 +9,6 @@ namespace Pterodactyl\Console\Commands\Environment; -use Ramsey\Uuid\Uuid; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; use Pterodactyl\Traits\Commands\EnvironmentWriterTrait; @@ -38,6 +37,7 @@ class AppSettingsCommand extends Command * @var string */ protected $signature = 'p:environment:setup + {--author= : The email that services created on this instance should be linked to.} {--url= : The URL that this Panel is running on.} {--timezone= : The timezone to use for Panel times.} {--cache= : The cache driver backend to use.} @@ -72,9 +72,10 @@ class AppSettingsCommand extends Command */ public function handle() { - if (is_null($this->config->get('pterodactyl.service.author'))) { - $this->variables['SERVICE_AUTHOR'] = Uuid::uuid4()->toString(); - } + $this->output->comment(trans('command/messages.environment.app.author_help')); + $this->variables['SERVICE_AUTHOR'] = $this->option('author') ?? $this->ask( + trans('command/messages.environment.app.author'), $this->config->get('pterodactyl.service.author', 'undefined@unknown-author.com') + ); $this->output->comment(trans('command/messages.environment.app.app_url_help')); $this->variables['APP_URL'] = $this->option('url') ?? $this->ask( diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php index 87919ae7a..7f710e624 100644 --- a/app/Contracts/Repository/ServiceRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -9,6 +9,9 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\Service; +use Illuminate\Support\Collection; + interface ServiceRepositoryInterface extends RepositoryInterface { /** @@ -17,7 +20,15 @@ interface ServiceRepositoryInterface extends RepositoryInterface * @param int $id * @return \Illuminate\Support\Collection */ - public function getWithOptions($id = null); + public function getWithOptions(int $id = null): Collection; + + /** + * Return a service or all services and the count of options, packs, and servers for that service. + * + * @param int|null $id + * @return \Illuminate\Support\Collection + */ + public function getWithCounts(int $id = null): Collection; /** * Return a service along with its associated options and the servers relation on those options. @@ -25,5 +36,5 @@ interface ServiceRepositoryInterface extends RepositoryInterface * @param int $id * @return mixed */ - public function getWithOptionServers($id); + public function getWithOptionServers(int $id): Service; } diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index 35b5155f6..72332c7b3 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -9,7 +9,9 @@ namespace Pterodactyl\Http\Controllers\Admin; +use Illuminate\View\View; use Pterodactyl\Models\Service; +use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Services\ServiceUpdateService; @@ -46,6 +48,15 @@ class ServiceController extends Controller */ protected $updateService; + /** + * ServiceController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Services\ServiceCreationService $creationService + * @param \Pterodactyl\Services\Services\ServiceDeletionService $deletionService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + * @param \Pterodactyl\Services\Services\ServiceUpdateService $updateService + */ public function __construct( AlertsMessageBag $alert, ServiceCreationService $creationService, @@ -65,10 +76,10 @@ class ServiceController extends Controller * * @return \Illuminate\View\View */ - public function index() + public function index(): View { return view('admin.services.index', [ - 'services' => $this->repository->getWithOptions(), + 'services' => $this->repository->getWithCounts(), ]); } @@ -77,7 +88,7 @@ class ServiceController extends Controller * * @return \Illuminate\View\View */ - public function create() + public function create(): View { return view('admin.services.new'); } @@ -88,7 +99,7 @@ class ServiceController extends Controller * @param int $service * @return \Illuminate\View\View */ - public function view($service) + public function view(int $service): View { return view('admin.services.view', [ 'service' => $this->repository->getWithOptionServers($service), @@ -101,7 +112,7 @@ class ServiceController extends Controller * @param \Pterodactyl\Models\Service $service * @return \Illuminate\View\View */ - public function viewFunctions(Service $service) + public function viewFunctions(Service $service): View { return view('admin.services.functions', ['service' => $service]); } @@ -114,7 +125,7 @@ class ServiceController extends Controller * * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(ServiceFormRequest $request) + public function store(ServiceFormRequest $request): RedirectResponse { $service = $this->creationService->handle($request->normalize()); $this->alert->success(trans('admin/services.notices.service_created', ['name' => $service->name]))->flash(); @@ -132,7 +143,7 @@ class ServiceController extends Controller * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(ServiceFormRequest $request, Service $service) + public function update(ServiceFormRequest $request, Service $service): RedirectResponse { $this->updateService->handle($service->id, $request->normalize()); $this->alert->success(trans('admin/services.notices.service_updated'))->flash(); @@ -150,7 +161,7 @@ class ServiceController extends Controller * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function updateFunctions(ServiceFunctionsFormRequest $request, Service $service) + public function updateFunctions(ServiceFunctionsFormRequest $request, Service $service): RedirectResponse { $this->updateService->handle($service->id, $request->normalize()); $this->alert->success(trans('admin/services.notices.functions_updated'))->flash(); @@ -166,7 +177,7 @@ class ServiceController extends Controller * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function destroy(Service $service) + public function destroy(Service $service): RedirectResponse { $this->deletionService->handle($service->id); $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 365f40d2b..7d5859981 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -13,7 +13,12 @@ use Illuminate\Foundation\Http\FormRequest; abstract class AdminFormRequest extends FormRequest { - abstract public function rules(); + /** + * The rules to apply to the incoming form request. + * + * @return array + */ + abstract public function rules(): array; /** * Determine if the user is an admin and has permission to access this @@ -21,7 +26,7 @@ abstract class AdminFormRequest extends FormRequest * * @return bool */ - public function authorize() + public function authorize(): bool { if (is_null($this->user())) { return false; @@ -37,7 +42,7 @@ abstract class AdminFormRequest extends FormRequest * @param array $only * @return array */ - public function normalize($only = []) + public function normalize($only = []): array { return array_merge( $this->only($only), diff --git a/app/Http/Requests/Admin/Service/ServiceFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFormRequest.php index c5af2b697..672f2d2f9 100644 --- a/app/Http/Requests/Admin/Service/ServiceFormRequest.php +++ b/app/Http/Requests/Admin/Service/ServiceFormRequest.php @@ -16,22 +16,12 @@ class ServiceFormRequest extends AdminFormRequest /** * @return array */ - public function rules() + public function rules(): array { - $rules = [ + return [ 'name' => 'required|string|min:1|max:255', 'description' => 'required|nullable|string', - 'folder' => 'required|regex:/^[\w.-]{1,50}$/|unique:services,folder', 'startup' => 'required|nullable|string', ]; - - if ($this->method() === 'PATCH') { - $service = $this->route()->parameter('service'); - $rules['folder'] = $rules['folder'] . ',' . $service->id; - - return $rules; - } - - return $rules; } } diff --git a/app/Models/Service.php b/app/Models/Service.php index e81a9ba90..0a17f7e42 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -31,7 +31,12 @@ class Service extends Model implements CleansAttributes, ValidableContract * * @var array */ - protected $fillable = ['name', 'author', 'description', 'folder', 'startup', 'index_file']; + protected $fillable = [ + 'name', + 'description', + 'startup', + 'index_file', + ]; /** * @var array @@ -40,7 +45,6 @@ class Service extends Model implements CleansAttributes, ValidableContract 'author' => 'required', 'name' => 'required', 'description' => 'sometimes', - 'folder' => 'required', 'startup' => 'sometimes', 'index_file' => 'required', ]; @@ -49,10 +53,9 @@ class Service extends Model implements CleansAttributes, ValidableContract * @var array */ protected static $dataIntegrityRules = [ - 'author' => 'string|size:36', + 'author' => 'email', 'name' => 'string|max:255', 'description' => 'nullable|string', - 'folder' => 'string|max:255|regex:/^[\w.-]{1,50}$/|unique:services,folder', 'startup' => 'nullable|string', 'index_file' => 'string', ]; @@ -74,12 +77,7 @@ class Service extends Model implements CleansAttributes, ValidableContract */ public function packs() { - return $this->hasManyThrough( - Pack::class, - ServiceOption::class, - 'service_id', - 'option_id' - ); + return $this->hasManyThrough(Pack::class, ServiceOption::class, 'service_id', 'option_id'); } /** diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index d59ca8f61..db5ec4c1f 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -31,7 +31,22 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @var array */ - protected $guarded = ['id', 'created_at', 'updated_at']; + protected $fillable = [ + 'name', + 'description', + 'docker_image', + 'config_files', + 'config_startup', + 'config_logs', + 'config_stop', + 'config_from', + 'startup', + 'script_is_privileged', + 'script_install', + 'script_entry', + 'script_container', + 'copy_script_from', + ]; /** * Cast values to correct type. @@ -40,7 +55,9 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract */ protected $casts = [ 'service_id' => 'integer', + 'config_from' => 'integer', 'script_is_privileged' => 'boolean', + 'copy_script_from' => 'integer', ]; /** @@ -48,6 +65,7 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract */ protected static $applicationRules = [ 'service_id' => 'required', + 'author' => 'required', 'name' => 'required', 'description' => 'required', 'tag' => 'required', @@ -64,13 +82,14 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * @var array */ protected static $dataIntegrityRules = [ - 'service_id' => 'numeric|exists:services,id', + 'service_id' => 'bail|numeric|exists:services,id', + 'author' => 'email', 'name' => 'string|max:255', 'description' => 'string', - 'tag' => 'alpha_num|max:60|unique:service_options,tag', + 'tag' => 'bail|alpha_num|max:60|unique:service_options,tag', 'docker_image' => 'string|max:255', 'startup' => 'nullable|string', - 'config_from' => 'nullable|numeric|exists:service_options,id', + 'config_from' => 'bail|nullable|numeric|exists:service_options,id', 'config_stop' => 'nullable|string|max:255', 'config_startup' => 'nullable|json', 'config_logs' => 'nullable|json', diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php index affe52de4..95d9224e5 100644 --- a/app/Repositories/Eloquent/ServiceRepository.php +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -9,8 +9,8 @@ namespace Pterodactyl\Repositories\Eloquent; -use Webmozart\Assert\Assert; use Pterodactyl\Models\Service; +use Illuminate\Support\Collection; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -27,16 +27,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI /** * {@inheritdoc} */ - public function getWithOptions($id = null) + public function getWithOptions(int $id = null): Collection { - Assert::nullOrNumeric($id, 'First argument passed to getWithOptions must be null or numeric, received %s.'); - $instance = $this->getBuilder()->with('options.packs', 'options.variables'); if (! is_null($id)) { $instance = $instance->find($id, $this->getColumns()); if (! $instance) { - throw new RecordNotFoundException(); + throw new RecordNotFoundException; } return $instance; @@ -48,15 +46,33 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI /** * {@inheritdoc} */ - public function getWithOptionServers($id) + public function getWithCounts(int $id = null): Collection { - Assert::numeric($id, 'First argument passed to getWithOptionServers must be numeric, received %s.'); + $instance = $this->getBuilder()->withCount(['options', 'packs', 'servers']); - $instance = $this->getBuilder()->with('options.servers')->find($id, $this->getColumns()); - if (! $instance) { - throw new RecordNotFoundException(); + if (! is_null($id)) { + $instance = $instance->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; } + return $instance->get($this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getWithOptionServers(int $id): Service + { + $instance = $this->getBuilder()->with('options.servers')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + /* @var Service $instance */ return $instance; } } diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index 7b302190e..abb7cfca1 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -40,7 +40,7 @@ class InstallScriptUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException */ - public function handle($option, array $data) + public function handle($option, array $data): void { if (! $option instanceof ServiceOption) { $option = $this->repository->find($option); diff --git a/app/Services/Services/Options/OptionCreationService.php b/app/Services/Services/Options/OptionCreationService.php index d15a813e4..8149a5596 100644 --- a/app/Services/Services/Options/OptionCreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -9,11 +9,19 @@ namespace Pterodactyl\Services\Services\Options; +use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\ServiceOption; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionCreationService { + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + /** * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface */ @@ -22,10 +30,12 @@ class OptionCreationService /** * CreationService constructor. * + * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository */ - public function __construct(ServiceOptionRepositoryInterface $repository) + public function __construct(ConfigRepository $config, ServiceOptionRepositoryInterface $repository) { + $this->config = $config; $this->repository = $repository; } @@ -38,7 +48,7 @@ class OptionCreationService * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException */ - public function handle(array $data) + public function handle(array $data): ServiceOption { if (! is_null(array_get($data, 'config_from'))) { $results = $this->repository->findCountWhere([ @@ -53,6 +63,14 @@ class OptionCreationService $data['config_from'] = null; } - return $this->repository->create($data); + if (count($parts = explode(':', array_get($data, 'tag'))) > 1) { + $data['tag'] = $this->config->get('pterodactyl.service.author') . ':' . trim(array_pop($parts)); + } else { + $data['tag'] = $this->config->get('pterodactyl.service.author') . ':' . trim(array_get($data, 'tag')); + } + + return $this->repository->create(array_merge($data, [ + 'uuid' => Uuid::uuid4()->toString(), + ]), true, true); } } diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index 27788ca5c..626c9d756 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -9,7 +9,6 @@ namespace Pterodactyl\Services\Services\Options; -use Webmozart\Assert\Assert; use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; @@ -50,10 +49,8 @@ class OptionDeletionService * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\HasChildrenException */ - public function handle($option) + public function handle(int $option): int { - Assert::integerish($option, 'First argument passed to handle must be integer, received %s.'); - $servers = $this->serverRepository->findCountWhere([['option_id', '=', $option]]); if ($servers > 0) { throw new HasActiveServersException(trans('exceptions.service.options.delete_has_servers')); diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index 1d2109de5..73c69cc5d 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -40,7 +40,7 @@ class OptionUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException */ - public function handle($option, array $data) + public function handle($option, array $data): void { if (! $option instanceof ServiceOption) { $option = $this->repository->find($option); diff --git a/app/Services/Services/ServiceCreationService.php b/app/Services/Services/ServiceCreationService.php index 4d7e77f1a..b0ddc62fb 100644 --- a/app/Services/Services/ServiceCreationService.php +++ b/app/Services/Services/ServiceCreationService.php @@ -9,6 +9,8 @@ namespace Pterodactyl\Services\Services; +use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\Service; use Pterodactyl\Traits\Services\CreatesServiceIndex; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -49,16 +51,15 @@ class ServiceCreationService * * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function handle(array $data) + public function handle(array $data): Service { - return $this->repository->create(array_merge([ + return $this->repository->create([ + 'uuid' => Uuid::uuid4()->toString(), 'author' => $this->config->get('pterodactyl.service.author'), - ], [ 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description'), - 'folder' => array_get($data, 'folder'), 'startup' => array_get($data, 'startup'), 'index_file' => $this->getIndexScript(), - ])); + ], true, true); } } diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php index def352b8d..0a88b1209 100644 --- a/app/Services/Services/ServiceDeletionService.php +++ b/app/Services/Services/ServiceDeletionService.php @@ -47,7 +47,7 @@ class ServiceDeletionService * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function handle($service) + public function handle(int $service): int { $count = $this->serverRepository->findCountWhere([['service_id', '=', $service]]); if ($count > 0) { diff --git a/app/Services/Services/ServiceUpdateService.php b/app/Services/Services/ServiceUpdateService.php index 59662e2a5..327c45aa7 100644 --- a/app/Services/Services/ServiceUpdateService.php +++ b/app/Services/Services/ServiceUpdateService.php @@ -36,7 +36,7 @@ class ServiceUpdateService * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($service, array $data) + public function handle(int $service, array $data): void { if (! is_null(array_get($data, 'author'))) { unset($data['author']); diff --git a/resources/themes/pterodactyl/admin/services/index.blade.php b/resources/themes/pterodactyl/admin/services/index.blade.php index cd336f9b7..3584f1f63 100644 --- a/resources/themes/pterodactyl/admin/services/index.blade.php +++ b/resources/themes/pterodactyl/admin/services/index.blade.php @@ -18,6 +18,13 @@ @endsection @section('content') +
+
+
+ Services are a powerful feature of Pterodactyl Panel that allow for extreme flexibility and configuration. Please note that while powerful, modifing a service wrongly can very easily brick your servers and cause more problems. Please avoid editing our default services — those provided by support@pterodactyl.io — unless you are absolutely sure of what you are doing. +
+
+
@@ -38,7 +45,7 @@ @foreach($services as $service) - {{ $service->name }} + {{ $service->name }} {{ $service->description }} {{ $service->options_count }} {{ $service->packs_count }} diff --git a/resources/themes/pterodactyl/admin/services/new.blade.php b/resources/themes/pterodactyl/admin/services/new.blade.php index 864dadbae..c63bec43b 100644 --- a/resources/themes/pterodactyl/admin/services/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/new.blade.php @@ -46,13 +46,6 @@
-
- -
- -

Service are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

-
-
diff --git a/resources/themes/pterodactyl/admin/services/options/new.blade.php b/resources/themes/pterodactyl/admin/services/options/new.blade.php index 2a5f3da9f..79195f008 100644 --- a/resources/themes/pterodactyl/admin/services/options/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/new.blade.php @@ -33,7 +33,7 @@
@@ -51,16 +51,19 @@
- +
+ {{ config('pterodactyl.service.author') }}: + +

This should be a unique identifer for this service option that is not used for any other service options. Must be alpha-numeric and no more than 60 characters in length.

- +

The default docker image that should be used for new servers under this service option. This can be left blank to use the parent service's defined image, and can also be changed per-server.

- +

The default statup command that should be used for new servers under this service option. This can be left blank to use the parent service's startup, and can also be changed per-server.

@@ -136,7 +139,7 @@ data: $.map(_.get(Pterodactyl.services, $(this).val() + '.options', []), function (item) { return { id: item.id, - text: item.name, + text: item.name + ' <' + item.tag + '>', }; }), }); diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php index b44b96c9a..1f6564d81 100644 --- a/resources/themes/pterodactyl/admin/services/options/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -59,17 +59,16 @@
- - -

This should be a unique identifer for this service option that is not used for any other service options.

+ +
- +

The default docker image that should be used for new servers under this service option. This can be left blank to use the parent service's defined image, and can also be changed per-server.

- +

The default statup command that should be used for new servers under this service option. This can be left blank to use the parent service's startup, and can also be changed per-server.

@@ -97,7 +96,7 @@

If you would like to default to settings from another option select the option from the menu above.

diff --git a/resources/themes/pterodactyl/admin/services/view.blade.php b/resources/themes/pterodactyl/admin/services/view.blade.php index f8461e7d2..2a6886f01 100644 --- a/resources/themes/pterodactyl/admin/services/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/view.blade.php @@ -44,22 +44,18 @@
- +
+
-
- -
- -

Service are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

-
-
@@ -67,10 +63,21 @@

The default start command to use when running options under this service. This command can be modified per-option and should include the executable to be called in the container.

+
+ +
+ +
+
+
+ +
+ +
+
@@ -86,7 +93,7 @@
- + diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index bfd97b439..c547e2762 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -197,6 +197,12 @@ }); @endif + + @show From 12faf80fafe66fb07e056497aa667eacbb68f97f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 2 Oct 2017 22:56:12 -0500 Subject: [PATCH 04/34] Disable seeding until more complete --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b0590806e..a29a33663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_install: before_script: - cp .env.travis .env - composer install --no-interaction --prefer-dist --no-suggest --verbose - - php artisan migrate --seed -v + - php artisan migrate -v script: - vendor/bin/phpunit --coverage-clover coverage.xml notifications: From 0d739257a9d164a3a97260e095aa45b79c04a84e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 00:01:04 -0500 Subject: [PATCH 05/34] First pass at XML exporter for services --- .../Options/OptionShareController.php | 45 +++++++ .../Services/Exporter/XMLExporterService.php | 118 ++++++++++++++++++ composer.json | 1 + composer.lock | 116 ++++++++++++++++- .../admin/services/options/view.blade.php | 3 +- .../pterodactyl/admin/services/view.blade.php | 2 +- routes/admin.php | 1 + 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Admin/Services/Options/OptionShareController.php create mode 100644 app/Services/Services/Exporter/XMLExporterService.php diff --git a/app/Http/Controllers/Admin/Services/Options/OptionShareController.php b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php new file mode 100644 index 000000000..bdf8c8c41 --- /dev/null +++ b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php @@ -0,0 +1,45 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Http\Controllers\Admin\Services\Options; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Services\Exporter\XMLExporterService; + +class OptionShareController extends Controller +{ + /** + * @var \Pterodactyl\Services\Services\Exporter\XMLExporterService + */ + protected $exporterService; + + /** + * OptionShareController constructor. + * + * @param \Pterodactyl\Services\Services\Exporter\XMLExporterService $exporterService + */ + public function __construct(XMLExporterService $exporterService) + { + $this->exporterService = $exporterService; + } + + /** + * @param \Pterodactyl\Models\ServiceOption $option + * @return $this + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function export(ServiceOption $option) + { + return response($this->exporterService->handle($option), 200, [ + 'Content-Type' => 'application/xml', + ]); + } +} diff --git a/app/Services/Services/Exporter/XMLExporterService.php b/app/Services/Services/Exporter/XMLExporterService.php new file mode 100644 index 000000000..5570a45c5 --- /dev/null +++ b/app/Services/Services/Exporter/XMLExporterService.php @@ -0,0 +1,118 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Services\Exporter; + +use Carbon\Carbon; +use Sabre\Xml\Writer; +use Sabre\Xml\Service; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class XMLExporterService +{ + const XML_OPTION_NAMESPACE = '{https://pterodactyl.io/exporter/option/}'; + + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Sabre\Xml\Service + */ + protected $xml; + + /** + * XMLExporterService constructor. + * + * @param \Carbon\Carbon $carbon + * @param \Sabre\Xml\Service $xml + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct( + Carbon $carbon, + Service $xml, + ServiceOptionRepositoryInterface $repository + ) { + $this->carbon = $carbon; + $this->repository = $repository; + $this->xml = $xml; + + $this->xml->namespaceMap = [ + str_replace(['{', '}'], '', self::XML_OPTION_NAMESPACE) => 'p', + ]; + } + + /** + * Return an XML structure to represent this service option. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @return string + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($option): string + { + if (! $option instanceof ServiceOption) { + $option = $this->repository->find($option); + } + + $struct = [ + 'exported_at' => $this->carbon->now()->toIso8601String(), + 'name' => $option->name, + 'author' => array_get(explode(':', $option->tag), 0), + 'tag' => $option->tag, + 'description' => $option->description, + 'image' => $option->docker_image, + 'config' => [ + 'files' => $option->config_files, + 'startup' => $option->config_startup, + 'logs' => $option->config_logs, + 'stop' => $option->config_stop, + ], + 'scripts' => [ + 'installation' => [ + 'script' => function (Writer $writer) use ($option) { + return $writer->writeCData($option->copy_script_install); + }, + ], + ], + ]; + + return $this->xml->write(self::XML_OPTION_NAMESPACE . 'root', $this->recursiveArrayKeyPrepend($struct)); + } + + /** + * @param array $array + * @param string $prepend + * + * @return array + */ + protected function recursiveArrayKeyPrepend(array $array, $prepend = self::XML_OPTION_NAMESPACE): array + { + $parsed = []; + foreach ($array as $k => &$v) { + $k = $prepend . $k; + + if (is_array($v)) { + $v = $this->recursiveArrayKeyPrepend($v); + } + + $parsed[$k] = $v; + } + + return $parsed; + } +} diff --git a/composer.json b/composer.json index db04ade72..d0aa38e74 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", "s1lentium/iptools": "^1.1", + "sabre/xml": "^2.0", "sofa/eloquence": "~5.4.1", "spatie/laravel-fractal": "^4.0", "watson/validating": "^3.0", diff --git a/composer.lock b/composer.lock index 4c112e5f4..34bb845a4 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": "46a0a06ec8f3af50ed6ec05c2bb3b9a3", + "content-hash": "bc8c88f86ea043406bce2f8fddf704b3", "packages": [ { "name": "aws/aws-sdk-php", @@ -2510,6 +2510,120 @@ ], "time": "2016-08-21T15:57:09+00:00" }, + { + "name": "sabre/uri", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-uri.git", + "reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/a42126042c7dcb53e2978dadb6d22574d1359b4c", + "reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c", + "shasum": "" + }, + "require": { + "php": ">=7" + }, + "require-dev": { + "phpunit/phpunit": "^6.0", + "sabre/cs": "~1.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "time": "2017-02-20T20:02:35+00:00" + }, + { + "name": "sabre/xml", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/fruux/sabre-xml.git", + "reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/054292959a1f2b64c10c9c7a03a816ba1872b8a3", + "reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": ">=7.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "phpunit/phpunit": "*", + "sabre/cs": "~1.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Xml\\": "lib/" + }, + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "time": "2016-11-16T00:41:01+00:00" + }, { "name": "sofa/eloquence", "version": "5.4.1", diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php index 1f6564d81..641a28be7 100644 --- a/resources/themes/pterodactyl/admin/services/options/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -131,7 +131,8 @@ - + + Export Option Configuration diff --git a/resources/themes/pterodactyl/admin/services/view.blade.php b/resources/themes/pterodactyl/admin/services/view.blade.php index 2a6886f01..f93e75a73 100644 --- a/resources/themes/pterodactyl/admin/services/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/view.blade.php @@ -101,7 +101,7 @@ @foreach($service->options as $option) - + diff --git a/routes/admin.php b/routes/admin.php index d5f16e813..6e1e93703 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -155,6 +155,7 @@ Route::group(['prefix' => 'services'], function () { Route::get('/view/{service}/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}/export', 'Services\Options\OptionShareController@export')->name('admin.services.option.export'); Route::get('/option/{option}/variables', 'VariableController@view')->name('admin.services.option.variables'); Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); From d608c313c3a6763987b36325f81eabfbfd9fe232 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 20:18:27 -0500 Subject: [PATCH 06/34] Complete the service option export configuration --- .../ServiceOptionRepositoryInterface.php | 18 +++-- .../Controllers/Admin/OptionController.php | 2 +- .../Options/OptionShareController.php | 10 ++- app/Models/ServiceOption.php | 66 ++++++++++++++++--- .../Eloquent/ServiceOptionRepository.php | 43 +++++++++--- .../Services/Exporter/XMLExporterService.php | 42 ++++++++---- 6 files changed, 141 insertions(+), 40 deletions(-) diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index 825c63d8b..331073e87 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -9,23 +9,29 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\ServiceOption; + interface ServiceOptionRepositoryInterface extends RepositoryInterface { /** * Return a service option with the variables relation attached. * * @param int $id - * @return mixed + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithVariables($id); + public function getWithVariables(int $id): ServiceOption; /** - * Return a service option with the copyFrom relation loaded onto the model. + * Return a service option with the scriptFrom and configFrom relations loaded onto the model. * * @param int $id - * @return mixed + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyFrom($id); + public function getWithCopyAttributes(int $id): ServiceOption; /** * Confirm a copy script belongs to the same service as the item trying to use it. @@ -34,5 +40,5 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface * @param int $service * @return bool */ - public function isCopiableScript($copyFromId, $service); + public function isCopiableScript(int $copyFromId, int $service): bool; } diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index f35c47119..3919fe9bf 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -163,7 +163,7 @@ class OptionController extends Controller */ public function viewScripts($option) { - $option = $this->serviceOptionRepository->getWithCopyFrom($option); + $option = $this->serviceOptionRepository->getWithCopyAttributes($option); $copyOptions = $this->serviceOptionRepository->findWhere([ ['copy_script_from', '=', null], ['service_id', '=', $option->service_id], diff --git a/app/Http/Controllers/Admin/Services/Options/OptionShareController.php b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php index bdf8c8c41..f46ee669d 100644 --- a/app/Http/Controllers/Admin/Services/Options/OptionShareController.php +++ b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php @@ -11,6 +11,7 @@ namespace Pterodactyl\Http\Controllers\Admin\Services\Options; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; +use Symfony\Component\HttpFoundation\Response; use Pterodactyl\Services\Services\Exporter\XMLExporterService; class OptionShareController extends Controller @@ -32,13 +33,16 @@ class OptionShareController extends Controller /** * @param \Pterodactyl\Models\ServiceOption $option - * @return $this + * @return \Symfony\Component\HttpFoundation\Response * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function export(ServiceOption $option) + public function export(ServiceOption $option): Response { - return response($this->exporterService->handle($option), 200, [ + return response($this->exporterService->handle($option->id), 200, [ + 'Content-Transfer-Encoding' => 'binary', + 'Content-Description' => 'File Transfer', + 'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.xml', 'Content-Type' => 'application/xml', ]); } diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index db5ec4c1f..1dd98c322 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -114,7 +114,7 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @return string */ - public function getDisplayStartupAttribute($value) + public function getDisplayStartupAttribute() { return (is_null($this->startup)) ? $this->service->startup : $this->startup; } @@ -125,9 +125,9 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @return string */ - public function getCopyScriptInstallAttribute($value) + public function getCopyScriptInstallAttribute() { - return (is_null($this->copy_script_from)) ? $this->script_install : $this->copyFrom->script_install; + return (is_null($this->copy_script_from)) ? $this->script_install : $this->scriptFrom->script_install; } /** @@ -136,9 +136,9 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @return string */ - public function getCopyScriptEntryAttribute($value) + public function getCopyScriptEntryAttribute() { - return (is_null($this->copy_script_from)) ? $this->script_entry : $this->copyFrom->script_entry; + return (is_null($this->copy_script_from)) ? $this->script_entry : $this->scriptFrom->script_entry; } /** @@ -147,9 +147,49 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @return string */ - public function getCopyScriptContainerAttribute($value) + public function getCopyScriptContainerAttribute() { - return (is_null($this->copy_script_from)) ? $this->script_container : $this->copyFrom->script_container; + return (is_null($this->copy_script_from)) ? $this->script_container : $this->scriptFrom->script_container; + } + + /** + * Return the file configuration for a service option. + * + * @return string + */ + public function getInheritConfigFilesAttribute() + { + return is_null($this->config_from) ? $this->config_files : $this->configFrom->config_files; + } + + /** + * Return the startup configuration for a service option. + * + * @return string + */ + public function getInheritConfigStartupAttribute() + { + return is_null($this->config_from) ? $this->config_startup : $this->configFrom->config_startup; + } + + /** + * Return the log reading configuration for a service option. + * + * @return string + */ + public function getInheritConfigLogsAttribute() + { + return is_null($this->config_from) ? $this->config_logs : $this->configFrom->config_logs; + } + + /** + * Return the stop command configuration for a service option. + * + * @return string + */ + public function getInheritConfigStopAttribute() + { + return is_null($this->config_from) ? $this->config_stop : $this->configFrom->config_stop; } /** @@ -197,8 +237,18 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ - public function copyFrom() + public function scriptFrom() { return $this->belongsTo(self::class, 'copy_script_from'); } + + /** + * Get the parent service option from which to copy configuration settings. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function configFrom() + { + return $this->belongsTo(self::class, 'config_from'); + } } diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index a2bcd0eb8..623899890 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; class ServiceOptionRepository extends EloquentRepository implements ServiceOptionRepositoryInterface @@ -23,25 +24,51 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio } /** - * {@inheritdoc} + * Return a service option with the variables relation attached. + * + * @param int $id + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithVariables($id) + public function getWithVariables(int $id): ServiceOption { - return $this->getBuilder()->with('variables')->find($id, $this->getColumns()); + /** @var \Pterodactyl\Models\ServiceOption $instance */ + $instance = $this->getBuilder()->with('variables')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; } /** - * {@inheritdoc} + * Return a service option with the scriptFrom and configFrom relations loaded onto the model. + * + * @param int $id + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyFrom($id) + public function getWithCopyAttributes(int $id): ServiceOption { - return $this->getBuilder()->with('copyFrom')->find($id, $this->getColumns()); + /** @var \Pterodactyl\Models\ServiceOption $instance */ + $instance = $this->getBuilder()->with('scriptFrom', 'configFrom')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; } /** - * {@inheritdoc} + * 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) + public function isCopiableScript(int $copyFromId, int $service): bool { return $this->getBuilder()->whereNull('copy_script_from') ->where('id', '=', $copyFromId) diff --git a/app/Services/Services/Exporter/XMLExporterService.php b/app/Services/Services/Exporter/XMLExporterService.php index 5570a45c5..ca621fc2e 100644 --- a/app/Services/Services/Exporter/XMLExporterService.php +++ b/app/Services/Services/Exporter/XMLExporterService.php @@ -9,10 +9,10 @@ namespace Pterodactyl\Services\Services\Exporter; +use Closure; use Carbon\Carbon; use Sabre\Xml\Writer; use Sabre\Xml\Service; -use Pterodactyl\Models\ServiceOption; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; class XMLExporterService @@ -58,35 +58,36 @@ class XMLExporterService /** * Return an XML structure to represent this service option. * - * @param int|\Pterodactyl\Models\ServiceOption $option + * @param int $option * @return string * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($option): string + public function handle(int $option): string { - if (! $option instanceof ServiceOption) { - $option = $this->repository->find($option); - } + $option = $this->repository->getWithCopyAttributes($option); $struct = [ + 'meta' => [ + 'version' => 'PTDL_v1', + ], 'exported_at' => $this->carbon->now()->toIso8601String(), 'name' => $option->name, 'author' => array_get(explode(':', $option->tag), 0), 'tag' => $option->tag, - 'description' => $option->description, + 'description' => $this->writeCData($option->description), 'image' => $option->docker_image, 'config' => [ - 'files' => $option->config_files, - 'startup' => $option->config_startup, - 'logs' => $option->config_logs, - 'stop' => $option->config_stop, + 'files' => $this->writeCData($option->inherit_config_files), + 'startup' => $this->writeCData($option->inherit_config_startup), + 'logs' => $this->writeCData($option->inherit_config_logs), + 'stop' => $option->inherit_config_stop, ], 'scripts' => [ 'installation' => [ - 'script' => function (Writer $writer) use ($option) { - return $writer->writeCData($option->copy_script_install); - }, + 'script' => $this->writeCData($option->copy_script_install), + 'container' => $option->copy_script_container, + 'entrypoint' => $option->copy_script_entry, ], ], ]; @@ -115,4 +116,17 @@ class XMLExporterService return $parsed; } + + /** + * Return a closure to be used by the XML writer to generate a string wrapped in CDATA tags. + * + * @param string $value + * @return \Closure + */ + protected function writeCData(string $value): Closure + { + return function (Writer $writer) use ($value) { + return $writer->writeCData($value); + }; + } } From 6269a08db7565c905983703ce7e403b86aa0ec93 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 23:31:04 -0500 Subject: [PATCH 07/34] Finalize service option import/export --- .../ServiceOptionRepositoryInterface.php | 10 ++ .../Repository/ServiceRepositoryInterface.php | 17 ++- .../DuplicateOptionTagException.php | 16 +++ .../Options/OptionShareController.php | 43 +++++- .../Admin/Service/OptionImportFormRequest.php | 26 ++++ app/Models/ServiceOption.php | 5 +- .../Eloquent/ServiceOptionRepository.php | 19 +++ .../Eloquent/ServiceRepository.php | 26 +++- .../Services/Exporter/XMLExporterService.php | 132 ------------------ .../Sharing/ServiceOptionExporterService.php | 87 ++++++++++++ .../Sharing/ServiceOptionImporterService.php | 123 ++++++++++++++++ composer.json | 1 - composer.lock | 116 +-------------- resources/lang/en/exceptions.php | 5 + .../admin/services/index.blade.php | 49 ++++++- routes/admin.php | 1 + 16 files changed, 405 insertions(+), 271 deletions(-) create mode 100644 app/Exceptions/Service/ServiceOption/DuplicateOptionTagException.php create mode 100644 app/Http/Requests/Admin/Service/OptionImportFormRequest.php delete mode 100644 app/Services/Services/Exporter/XMLExporterService.php create mode 100644 app/Services/Services/Sharing/ServiceOptionExporterService.php create mode 100644 app/Services/Services/Sharing/ServiceOptionImporterService.php diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index 331073e87..c7acd5a45 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -33,6 +33,16 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface */ public function getWithCopyAttributes(int $id): ServiceOption; + /** + * Return all of the data needed to export a service. + * + * @param int $id + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithExportAttributes(int $id): ServiceOption; + /** * Confirm a copy script belongs to the same service as the item trying to use it. * diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php index 7f710e624..c514c1cf7 100644 --- a/app/Contracts/Repository/ServiceRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -10,7 +10,6 @@ namespace Pterodactyl\Contracts\Repository; use Pterodactyl\Models\Service; -use Illuminate\Support\Collection; interface ServiceRepositoryInterface extends RepositoryInterface { @@ -18,23 +17,29 @@ interface ServiceRepositoryInterface extends RepositoryInterface * Return a service or all services with their associated options, variables, and packs. * * @param int $id - * @return \Illuminate\Support\Collection + * @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithOptions(int $id = null): Collection; + public function getWithOptions(int $id = null); /** * Return a service or all services and the count of options, packs, and servers for that service. * * @param int|null $id - * @return \Illuminate\Support\Collection + * @return \Pterodactyl\Models\Service|\Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCounts(int $id = null): Collection; + public function getWithCounts(int $id = null); /** * Return a service along with its associated options and the servers relation on those options. * * @param int $id - * @return mixed + * @return \Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getWithOptionServers(int $id): Service; } diff --git a/app/Exceptions/Service/ServiceOption/DuplicateOptionTagException.php b/app/Exceptions/Service/ServiceOption/DuplicateOptionTagException.php new file mode 100644 index 000000000..070beda42 --- /dev/null +++ b/app/Exceptions/Service/ServiceOption/DuplicateOptionTagException.php @@ -0,0 +1,16 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Exceptions\Service\ServiceOption; + +use Pterodactyl\Exceptions\DisplayException; + +class DuplicateOptionTagException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Admin/Services/Options/OptionShareController.php b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php index f46ee669d..76f58eb70 100644 --- a/app/Http/Controllers/Admin/Services/Options/OptionShareController.php +++ b/app/Http/Controllers/Admin/Services/Options/OptionShareController.php @@ -9,26 +9,38 @@ namespace Pterodactyl\Http\Controllers\Admin\Services\Options; +use Illuminate\Http\RedirectResponse; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; use Symfony\Component\HttpFoundation\Response; -use Pterodactyl\Services\Services\Exporter\XMLExporterService; +use Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest; +use Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService; +use Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService; class OptionShareController extends Controller { /** - * @var \Pterodactyl\Services\Services\Exporter\XMLExporterService + * @var \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService */ protected $exporterService; + /** + * @var \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService + */ + protected $importerService; + /** * OptionShareController constructor. * - * @param \Pterodactyl\Services\Services\Exporter\XMLExporterService $exporterService + * @param \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService $exporterService + * @param \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService $importerService */ - public function __construct(XMLExporterService $exporterService) - { + public function __construct( + ServiceOptionExporterService $exporterService, + ServiceOptionImporterService $importerService + ) { $this->exporterService = $exporterService; + $this->importerService = $importerService; } /** @@ -42,8 +54,25 @@ class OptionShareController extends Controller return response($this->exporterService->handle($option->id), 200, [ 'Content-Transfer-Encoding' => 'binary', 'Content-Description' => 'File Transfer', - 'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.xml', - 'Content-Type' => 'application/xml', + 'Content-Disposition' => 'attachment; filename=' . kebab_case($option->name) . '.json', + 'Content-Type' => 'application/json', ]); } + + /** + * Import a new service option using an XML file. + * + * @param \Pterodactyl\Http\Requests\Admin\Service\OptionImportFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + */ + public function import(OptionImportFormRequest $request): RedirectResponse + { + $option = $this->importerService->handle($request->file('import_file'), $request->input('import_to_service')); + + return redirect()->route('admin.services.option.view', ['option' => $option->id]); + } } diff --git a/app/Http/Requests/Admin/Service/OptionImportFormRequest.php b/app/Http/Requests/Admin/Service/OptionImportFormRequest.php new file mode 100644 index 000000000..8e90087ca --- /dev/null +++ b/app/Http/Requests/Admin/Service/OptionImportFormRequest.php @@ -0,0 +1,26 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class OptionImportFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules(): array + { + return [ + 'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain', + 'import_to_service' => 'bail|required|integer|exists:services,id', + ]; + } +} diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index 1dd98c322..f4bc72eaa 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -65,7 +65,6 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract */ protected static $applicationRules = [ 'service_id' => 'required', - 'author' => 'required', 'name' => 'required', 'description' => 'required', 'tag' => 'required', @@ -83,10 +82,10 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'service_id' => 'bail|numeric|exists:services,id', - 'author' => 'email', + 'uuid' => 'string|size:36', 'name' => 'string|max:255', 'description' => 'string', - 'tag' => 'bail|alpha_num|max:60|unique:service_options,tag', + 'tag' => 'bail|string|max:150', 'docker_image' => 'string|max:255', 'startup' => 'nullable|string', 'config_from' => 'bail|nullable|numeric|exists:service_options,id', diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index 623899890..5d8bec31d 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -61,6 +61,25 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio return $instance; } + /** + * Return all of the data needed to export a service. + * + * @param int $id + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithExportAttributes(int $id): ServiceOption + { + /** @var \Pterodactyl\Models\ServiceOption $instance */ + $instance = $this->getBuilder()->with('scriptFrom', 'configFrom', 'variables')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } + /** * Confirm a copy script belongs to the same service as the item trying to use it. * diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php index 95d9224e5..1a4fe659c 100644 --- a/app/Repositories/Eloquent/ServiceRepository.php +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -10,7 +10,6 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Service; -use Illuminate\Support\Collection; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -25,9 +24,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI } /** - * {@inheritdoc} + * Return a service or all services with their associated options, variables, and packs. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithOptions(int $id = null): Collection + public function getWithOptions(int $id = null) { $instance = $this->getBuilder()->with('options.packs', 'options.variables'); @@ -44,9 +48,14 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI } /** - * {@inheritdoc} + * Return a service or all services and the count of options, packs, and servers for that service. + * + * @param int|null $id + * @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCounts(int $id = null): Collection + public function getWithCounts(int $id = null) { $instance = $this->getBuilder()->withCount(['options', 'packs', 'servers']); @@ -63,7 +72,12 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI } /** - * {@inheritdoc} + * Return a service along with its associated options and the servers relation on those options. + * + * @param int $id + * @return \Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getWithOptionServers(int $id): Service { diff --git a/app/Services/Services/Exporter/XMLExporterService.php b/app/Services/Services/Exporter/XMLExporterService.php deleted file mode 100644 index ca621fc2e..000000000 --- a/app/Services/Services/Exporter/XMLExporterService.php +++ /dev/null @@ -1,132 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Services\Services\Exporter; - -use Closure; -use Carbon\Carbon; -use Sabre\Xml\Writer; -use Sabre\Xml\Service; -use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; - -class XMLExporterService -{ - const XML_OPTION_NAMESPACE = '{https://pterodactyl.io/exporter/option/}'; - - /** - * @var \Carbon\Carbon - */ - protected $carbon; - - /** - * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface - */ - protected $repository; - - /** - * @var \Sabre\Xml\Service - */ - protected $xml; - - /** - * XMLExporterService constructor. - * - * @param \Carbon\Carbon $carbon - * @param \Sabre\Xml\Service $xml - * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository - */ - public function __construct( - Carbon $carbon, - Service $xml, - ServiceOptionRepositoryInterface $repository - ) { - $this->carbon = $carbon; - $this->repository = $repository; - $this->xml = $xml; - - $this->xml->namespaceMap = [ - str_replace(['{', '}'], '', self::XML_OPTION_NAMESPACE) => 'p', - ]; - } - - /** - * Return an XML structure to represent this service option. - * - * @param int $option - * @return string - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle(int $option): string - { - $option = $this->repository->getWithCopyAttributes($option); - - $struct = [ - 'meta' => [ - 'version' => 'PTDL_v1', - ], - 'exported_at' => $this->carbon->now()->toIso8601String(), - 'name' => $option->name, - 'author' => array_get(explode(':', $option->tag), 0), - 'tag' => $option->tag, - 'description' => $this->writeCData($option->description), - 'image' => $option->docker_image, - 'config' => [ - 'files' => $this->writeCData($option->inherit_config_files), - 'startup' => $this->writeCData($option->inherit_config_startup), - 'logs' => $this->writeCData($option->inherit_config_logs), - 'stop' => $option->inherit_config_stop, - ], - 'scripts' => [ - 'installation' => [ - 'script' => $this->writeCData($option->copy_script_install), - 'container' => $option->copy_script_container, - 'entrypoint' => $option->copy_script_entry, - ], - ], - ]; - - return $this->xml->write(self::XML_OPTION_NAMESPACE . 'root', $this->recursiveArrayKeyPrepend($struct)); - } - - /** - * @param array $array - * @param string $prepend - * - * @return array - */ - protected function recursiveArrayKeyPrepend(array $array, $prepend = self::XML_OPTION_NAMESPACE): array - { - $parsed = []; - foreach ($array as $k => &$v) { - $k = $prepend . $k; - - if (is_array($v)) { - $v = $this->recursiveArrayKeyPrepend($v); - } - - $parsed[$k] = $v; - } - - return $parsed; - } - - /** - * Return a closure to be used by the XML writer to generate a string wrapped in CDATA tags. - * - * @param string $value - * @return \Closure - */ - protected function writeCData(string $value): Closure - { - return function (Writer $writer) use ($value) { - return $writer->writeCData($value); - }; - } -} diff --git a/app/Services/Services/Sharing/ServiceOptionExporterService.php b/app/Services/Services/Sharing/ServiceOptionExporterService.php new file mode 100644 index 000000000..744d298a4 --- /dev/null +++ b/app/Services/Services/Sharing/ServiceOptionExporterService.php @@ -0,0 +1,87 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Services\Sharing; + +use Carbon\Carbon; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class ServiceOptionExporterService +{ + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * XMLExporterService constructor. + * + * @param \Carbon\Carbon $carbon + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct( + Carbon $carbon, + ServiceOptionRepositoryInterface $repository + ) { + $this->carbon = $carbon; + $this->repository = $repository; + } + + /** + * Return an XML structure to represent this service option. + * + * @param int $option + * @return string + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(int $option): string + { + $option = $this->repository->getWithExportAttributes($option); + + $struct = [ + '_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO', + 'meta' => [ + 'version' => 'PTDL_v1', + ], + 'exported_at' => $this->carbon->now()->toIso8601String(), + 'name' => $option->name, + 'author' => array_get(explode(':', $option->tag), 0), + 'tag' => $option->tag, + 'description' => $option->description, + 'image' => $option->docker_image, + 'startup' => $option->display_startup, + 'config' => [ + 'files' => $option->inherit_config_files, + 'startup' => $option->inherit_config_startup, + 'logs' => $option->inherit_config_logs, + 'stop' => $option->inherit_config_stop, + ], + 'scripts' => [ + 'installation' => [ + 'script' => $option->copy_script_install, + 'container' => $option->copy_script_container, + 'entrypoint' => $option->copy_script_entry, + ], + ], + 'variables' => $option->variables->transform(function ($item) { + return collect($item->toArray())->except([ + 'id', 'option_id', 'created_at', 'updated_at', + ])->toArray(); + }), + ]; + + return json_encode($struct, JSON_PRETTY_PRINT); + } +} diff --git a/app/Services/Services/Sharing/ServiceOptionImporterService.php b/app/Services/Services/Sharing/ServiceOptionImporterService.php new file mode 100644 index 000000000..4061af285 --- /dev/null +++ b/app/Services/Services/Sharing/ServiceOptionImporterService.php @@ -0,0 +1,123 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Services\Sharing; + +use Ramsey\Uuid\Uuid; +use Illuminate\Http\UploadedFile; +use Pterodactyl\Models\ServiceOption; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\ServiceOption\DuplicateOptionTagException; + +class ServiceOptionImporterService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + /** + * XMLImporterService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface $serviceVariableRepository + */ + public function __construct( + ConnectionInterface $connection, + ServiceRepositoryInterface $serviceRepository, + ServiceOptionRepositoryInterface $repository, + ServiceVariableRepositoryInterface $serviceVariableRepository + ) { + $this->connection = $connection; + $this->repository = $repository; + $this->serviceRepository = $serviceRepository; + $this->serviceVariableRepository = $serviceVariableRepository; + } + + /** + * Take an uploaded XML file and parse it into a new service option. + * + * @param \Illuminate\Http\UploadedFile $file + * @param int $service + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + */ + public function handle(UploadedFile $file, int $service): ServiceOption + { + if (! $file->isValid() || ! $file->isFile()) { + throw new InvalidFileUploadException(trans('exceptions.service.exporter.import_file_error')); + } + + $parsed = json_decode($file->openFile()->fread($file->getSize())); + + if (object_get($parsed, 'meta.version') !== 'PTDL_v1') { + throw new InvalidFileUploadException(trans('exceptions.service.exporter.invalid_json_provided')); + } + + $service = $this->serviceRepository->getWithOptions($service); + $service->options->each(function ($option) use ($parsed) { + if ($option->tag === object_get($parsed, 'tag')) { + throw new DuplicateOptionTagException(trans('exceptions.service.options.duplicate_tag')); + } + }); + + $this->connection->beginTransaction(); + $option = $this->repository->create([ + 'uuid' => Uuid::uuid4()->toString(), + 'service_id' => $service->id, + 'name' => object_get($parsed, 'name'), + 'description' => object_get($parsed, 'description'), + 'tag' => object_get($parsed, 'tag'), + 'docker_image' => object_get($parsed, 'image'), + 'config_files' => object_get($parsed, 'config.files'), + 'config_startup' => object_get($parsed, 'config.startup'), + 'config_logs' => object_get($parsed, 'config.logs'), + 'config_stop' => object_get($parsed, 'config.stop'), + 'startup' => object_get($parsed, 'startup'), + 'script_install' => object_get($parsed, 'scripts.installation.script'), + 'script_entry' => object_get($parsed, 'scripts.installation.entrypoint'), + 'script_container' => object_get($parsed, 'scripts.installation.container'), + 'copy_script_from' => null, + ], true, true); + + collect($parsed->variables)->each(function ($variable) use ($option) { + $this->serviceVariableRepository->create(array_merge((array) $variable, [ + 'option_id' => $option->id, + ])); + }); + + $this->connection->commit(); + + return $option; + } +} diff --git a/composer.json b/composer.json index d0aa38e74..db04ade72 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,6 @@ "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", "s1lentium/iptools": "^1.1", - "sabre/xml": "^2.0", "sofa/eloquence": "~5.4.1", "spatie/laravel-fractal": "^4.0", "watson/validating": "^3.0", diff --git a/composer.lock b/composer.lock index 34bb845a4..4c112e5f4 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": "bc8c88f86ea043406bce2f8fddf704b3", + "content-hash": "46a0a06ec8f3af50ed6ec05c2bb3b9a3", "packages": [ { "name": "aws/aws-sdk-php", @@ -2510,120 +2510,6 @@ ], "time": "2016-08-21T15:57:09+00:00" }, - { - "name": "sabre/uri", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/fruux/sabre-uri.git", - "reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/a42126042c7dcb53e2978dadb6d22574d1359b4c", - "reference": "a42126042c7dcb53e2978dadb6d22574d1359b4c", - "shasum": "" - }, - "require": { - "php": ">=7" - }, - "require-dev": { - "phpunit/phpunit": "^6.0", - "sabre/cs": "~1.0.0" - }, - "type": "library", - "autoload": { - "files": [ - "lib/functions.php" - ], - "psr-4": { - "Sabre\\Uri\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "Functions for making sense out of URIs.", - "homepage": "http://sabre.io/uri/", - "keywords": [ - "rfc3986", - "uri", - "url" - ], - "time": "2017-02-20T20:02:35+00:00" - }, - { - "name": "sabre/xml", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/fruux/sabre-xml.git", - "reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/054292959a1f2b64c10c9c7a03a816ba1872b8a3", - "reference": "054292959a1f2b64c10c9c7a03a816ba1872b8a3", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "lib-libxml": ">=2.6.20", - "php": ">=7.0", - "sabre/uri": ">=1.0,<3.0.0" - }, - "require-dev": { - "phpunit/phpunit": "*", - "sabre/cs": "~1.0.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Sabre\\Xml\\": "lib/" - }, - "files": [ - "lib/Deserializer/functions.php", - "lib/Serializer/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - }, - { - "name": "Markus Staab", - "email": "markus.staab@redaxo.de", - "role": "Developer" - } - ], - "description": "sabre/xml is an XML library that you may not hate.", - "homepage": "https://sabre.io/xml/", - "keywords": [ - "XMLReader", - "XMLWriter", - "dom", - "xml" - ], - "time": "2016-11-16T00:41:01+00:00" - }, { "name": "sofa/eloquence", "version": "5.4.1", diff --git a/resources/lang/en/exceptions.php b/resources/lang/en/exceptions.php index 2a8f1e047..7d42e1621 100644 --- a/resources/lang/en/exceptions.php +++ b/resources/lang/en/exceptions.php @@ -21,6 +21,7 @@ return [ 'service' => [ 'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.', 'options' => [ + 'duplicate_tag' => 'A service option with that tag already exists for this 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.', @@ -30,6 +31,10 @@ return [ '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.', ], + 'exporter' => [ + 'import_file_error' => 'The XML file provided was not valid.', + 'invalid_json_provided' => 'The JSON file provided is not in a format that can be recognized.', + ], ], 'packs' => [ 'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.', diff --git a/resources/themes/pterodactyl/admin/services/index.blade.php b/resources/themes/pterodactyl/admin/services/index.blade.php index 3584f1f63..a1cd88212 100644 --- a/resources/themes/pterodactyl/admin/services/index.blade.php +++ b/resources/themes/pterodactyl/admin/services/index.blade.php @@ -31,7 +31,8 @@

Configured Service

@@ -57,4 +58,50 @@
+ +@endsection + +@section('footer-scripts') + @parent + @endsection diff --git a/routes/admin.php b/routes/admin.php index 6e1e93703..568fa0199 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -160,6 +160,7 @@ Route::group(['prefix' => 'services'], function () { Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); Route::post('/new', 'ServiceController@store'); + Route::post('/import', 'Services\Options\OptionShareController@import')->name('admin.services.option.import'); Route::post('/option/new', 'OptionController@store'); Route::post('/option/{option}/variables', 'VariableController@store'); From 9b79d9c756de51b4e0d2e9e7a0032103023f2c7a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 23:33:46 -0500 Subject: [PATCH 08/34] Delete service variables when the option is deleted. --- ...cadeDeletionWhenServiceOptionIsDeleted.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2017_10_03_233202_CascadeDeletionWhenServiceOptionIsDeleted.php diff --git a/database/migrations/2017_10_03_233202_CascadeDeletionWhenServiceOptionIsDeleted.php b/database/migrations/2017_10_03_233202_CascadeDeletionWhenServiceOptionIsDeleted.php new file mode 100644 index 000000000..3b19e3d99 --- /dev/null +++ b/database/migrations/2017_10_03_233202_CascadeDeletionWhenServiceOptionIsDeleted.php @@ -0,0 +1,32 @@ +dropForeign(['option_id']); + + $table->foreign('option_id')->references('id')->on('service_options')->onDelete('CASCADE'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('service_variables', function (Blueprint $table) { + $table->dropForeign(['option_id']); + + $table->foreign('option_id')->references('id')->on('service_options'); + }); + } +} From 8952043600eecf4171a68822e4be94be3784d996 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 23:36:39 -0500 Subject: [PATCH 09/34] Fix some test runner issues --- app/Http/Requests/Admin/AdminFormRequest.php | 2 +- app/Http/Requests/Admin/Service/OptionImportFormRequest.php | 2 +- app/Http/Requests/Admin/Service/ServiceFormRequest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 7d5859981..887583c7d 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -18,7 +18,7 @@ abstract class AdminFormRequest extends FormRequest * * @return array */ - abstract public function rules(): array; + abstract public function rules(); /** * Determine if the user is an admin and has permission to access this diff --git a/app/Http/Requests/Admin/Service/OptionImportFormRequest.php b/app/Http/Requests/Admin/Service/OptionImportFormRequest.php index 8e90087ca..5c464c658 100644 --- a/app/Http/Requests/Admin/Service/OptionImportFormRequest.php +++ b/app/Http/Requests/Admin/Service/OptionImportFormRequest.php @@ -16,7 +16,7 @@ class OptionImportFormRequest extends AdminFormRequest /** * @return array */ - public function rules(): array + public function rules() { return [ 'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain', diff --git a/app/Http/Requests/Admin/Service/ServiceFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFormRequest.php index 672f2d2f9..e469ecd54 100644 --- a/app/Http/Requests/Admin/Service/ServiceFormRequest.php +++ b/app/Http/Requests/Admin/Service/ServiceFormRequest.php @@ -16,7 +16,7 @@ class ServiceFormRequest extends AdminFormRequest /** * @return array */ - public function rules(): array + public function rules() { return [ 'name' => 'required|string|min:1|max:255', From 29ac1662b669aa0f81d8f4101fe098729f0f3eae Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 23:54:24 -0500 Subject: [PATCH 10/34] Fix failing tests --- .../Options/OptionCreationServiceTest.php | 75 +++++++++++++++---- .../Services/ServiceCreationServiceTest.php | 16 +++- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php index dac7e7dd3..c15db8f07 100644 --- a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -12,7 +12,9 @@ namespace Tests\Unit\Services\Services\Options; use Exception; use Mockery as m; use Tests\TestCase; +use Ramsey\Uuid\Uuid; use Pterodactyl\Models\ServiceOption; +use Illuminate\Contracts\Config\Repository; use Pterodactyl\Services\Services\Options\OptionCreationService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; @@ -20,12 +22,12 @@ use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundExcep class OptionCreationServiceTest extends TestCase { /** - * @var \Pterodactyl\Models\ServiceOption + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $model; + protected $config; /** - * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface|\Mockery\Mock */ protected $repository; @@ -34,6 +36,11 @@ class OptionCreationServiceTest extends TestCase */ protected $service; + /** + * @var \Ramsey\Uuid\Uuid|\Mockery\Mock + */ + protected $uuid; + /** * Setup tests. */ @@ -41,10 +48,11 @@ class OptionCreationServiceTest extends TestCase { parent::setUp(); - $this->model = factory(ServiceOption::class)->make(); + $this->config = m::mock(Repository::class); $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + $this->uuid = m::mock('overload:' . Uuid::class); - $this->service = new OptionCreationService($this->repository); + $this->service = new OptionCreationService($this->config, $this->repository); } /** @@ -52,14 +60,48 @@ class OptionCreationServiceTest extends TestCase */ public function testCreateNewModelWithoutUsingConfigFrom() { - $this->repository->shouldReceive('create')->with(['name' => $this->model->name, 'config_from' => null]) - ->once()->andReturn($this->model); + $model = factory(ServiceOption::class)->make(); - $response = $this->service->handle(['name' => $this->model->name]); + $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com'); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-string'); + $this->repository->shouldReceive('create')->with([ + 'name' => $model->name, + 'config_from' => null, + 'tag' => 'test@example.com:' . $model->tag, + 'uuid' => 'uuid-string', + ], true, true)->once()->andReturn($model); + + $response = $this->service->handle(['name' => $model->name, 'tag' => $model->tag]); $this->assertNotEmpty($response); $this->assertNull(object_get($response, 'config_from')); - $this->assertEquals($this->model->name, $response->name); + $this->assertEquals($model->name, $response->name); + } + + /** + * Test that passing a bad tag into the function will set the correct tag. + */ + public function testCreateNewModelUsingLongTagForm() + { + $model = factory(ServiceOption::class)->make([ + 'tag' => 'test@example.com:tag', + ]); + + $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com'); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-string'); + $this->repository->shouldReceive('create')->with([ + 'name' => $model->name, + 'config_from' => null, + 'tag' => $model->tag, + 'uuid' => 'uuid-string', + ], true, true)->once()->andReturn($model); + + $response = $this->service->handle(['name' => $model->name, 'tag' => 'bad@example.com:tag']); + + $this->assertNotEmpty($response); + $this->assertNull(object_get($response, 'config_from')); + $this->assertEquals($model->name, $response->name); + $this->assertEquals('test@example.com:tag', $response->tag); } /** @@ -67,10 +109,14 @@ class OptionCreationServiceTest extends TestCase */ public function testCreateNewModelUsingConfigFrom() { + $model = factory(ServiceOption::class)->make(); + $data = [ - 'name' => $this->model->name, - 'service_id' => $this->model->service_id, + 'name' => $model->name, + 'service_id' => $model->service_id, + 'tag' => 'test@example.com:tag', 'config_from' => 1, + 'uuid' => 'uuid-string', ]; $this->repository->shouldReceive('findCountWhere')->with([ @@ -78,13 +124,14 @@ class OptionCreationServiceTest extends TestCase ['id', '=', $data['config_from']], ])->once()->andReturn(1); - $this->repository->shouldReceive('create')->with($data) - ->once()->andReturn($this->model); + $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com'); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-string'); + $this->repository->shouldReceive('create')->with($data, true, true)->once()->andReturn($model); $response = $this->service->handle($data); $this->assertNotEmpty($response); - $this->assertEquals($response, $this->model); + $this->assertEquals($response, $model); } /** diff --git a/tests/Unit/Services/Services/ServiceCreationServiceTest.php b/tests/Unit/Services/Services/ServiceCreationServiceTest.php index c1e64d9b9..0abc4febf 100644 --- a/tests/Unit/Services/Services/ServiceCreationServiceTest.php +++ b/tests/Unit/Services/Services/ServiceCreationServiceTest.php @@ -11,6 +11,7 @@ namespace Tests\Unit\Services\Services; use Mockery as m; use Tests\TestCase; +use Ramsey\Uuid\Uuid; use Pterodactyl\Models\Service; use Illuminate\Contracts\Config\Repository; use Pterodactyl\Traits\Services\CreatesServiceIndex; @@ -22,12 +23,12 @@ class ServiceCreationServiceTest extends TestCase use CreatesServiceIndex; /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ protected $config; /** - * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface|\Mockery\Mock */ protected $repository; @@ -36,6 +37,11 @@ class ServiceCreationServiceTest extends TestCase */ protected $service; + /** + * @var \Ramsey\Uuid\Uuid|\Mockery\Mock + */ + protected $uuid; + /** * Setup tests. */ @@ -45,6 +51,7 @@ class ServiceCreationServiceTest extends TestCase $this->config = m::mock(Repository::class); $this->repository = m::mock(ServiceRepositoryInterface::class); + $this->uuid = m::mock('overload:' . Uuid::class); $this->service = new ServiceCreationService($this->config, $this->repository); } @@ -62,15 +69,16 @@ class ServiceCreationServiceTest extends TestCase 'startup' => $model->startup, ]; + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-0000'); $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('0000-author'); $this->repository->shouldReceive('create')->with([ + 'uuid' => 'uuid-0000', 'author' => '0000-author', 'name' => $data['name'], 'description' => $data['description'], - 'folder' => $data['folder'], 'startup' => $data['startup'], 'index_file' => $this->getIndexScript(), - ])->once()->andReturn($model); + ], true, true)->once()->andReturn($model); $response = $this->service->handle($data); $this->assertInstanceOf(Service::class, $response); From 3e689cf21255ef6ef54fa5af3e501b1632d09479 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 3 Oct 2017 23:57:13 -0500 Subject: [PATCH 11/34] Please fix failing tests? :hand: --- app/Http/Requests/Admin/AdminFormRequest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 887583c7d..7f7ce8cb7 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -26,7 +26,7 @@ abstract class AdminFormRequest extends FormRequest * * @return bool */ - public function authorize(): bool + public function authorize() { if (is_null($this->user())) { return false; @@ -42,7 +42,7 @@ abstract class AdminFormRequest extends FormRequest * @param array $only * @return array */ - public function normalize($only = []): array + public function normalize($only = []) { return array_merge( $this->only($only), From d95a63c09bda5df31fdc0255d66b91301233599e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 4 Oct 2017 22:41:15 -0500 Subject: [PATCH 12/34] Today Dane learned about Mockery::subset, tomorrow we take over Canada. In other news, why could no one have mentioned this a few months ago. Would have been nice. --- database/factories/ModelFactory.php | 5 +- .../ServiceOptionImporterServiceTest.php | 202 ++++++++++++++++++ 2 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Services/Services/Sharing/ServiceOptionImporterServiceTest.php diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 1d069a26d..d61f46cc0 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -103,10 +103,11 @@ $factory->define(Pterodactyl\Models\Service::class, function (Faker\Generator $f $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), + 'uuid' => $faker->unique()->uuid, 'service_id' => $faker->unique()->randomNumber(), 'name' => $faker->name, 'description' => implode(' ', $faker->sentences(3)), - 'tag' => $faker->unique()->randomNumber(5), + 'tag' => 'test@testfactory.com:' . $faker->unique()->randomNumber(8), ]; }); @@ -120,8 +121,6 @@ $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Gene 'user_viewable' => 0, 'user_editable' => 0, 'rules' => 'required|string', - 'created_at' => \Carbon\Carbon::now(), - 'updated_at' => \Carbon\Carbon::now(), ]; }); diff --git a/tests/Unit/Services/Services/Sharing/ServiceOptionImporterServiceTest.php b/tests/Unit/Services/Services/Sharing/ServiceOptionImporterServiceTest.php new file mode 100644 index 000000000..6dbdc5a53 --- /dev/null +++ b/tests/Unit/Services/Services/Sharing/ServiceOptionImporterServiceTest.php @@ -0,0 +1,202 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Tests\Unit\Services\Services\Sharing; + +use Mockery as m; +use Tests\TestCase; +use Ramsey\Uuid\Uuid; +use Pterodactyl\Models\Service; +use Illuminate\Http\UploadedFile; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\PterodactylException; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\ServiceOption\DuplicateOptionTagException; + +class ServiceOptionImporterServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + */ + protected $connection; + + /** + * @var \Illuminate\Http\UploadedFile|\Mockery\Mock + */ + protected $file; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface|\Mockery\Mock + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Sharing\ServiceOptionImporterService + */ + protected $service; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface|\Mockery\Mock + */ + protected $serviceRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface|\Mockery\Mock + */ + protected $serviceVariableRepository; + + /** + * @var \Ramsey\Uuid\Uuid|\Mockery\Mock + */ + protected $uuid; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->file = m::mock(UploadedFile::class); + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + $this->serviceRepository = m::mock(ServiceRepositoryInterface::class); + $this->serviceVariableRepository = m::mock(ServiceVariableRepositoryInterface::class); + $this->uuid = m::mock('overload:' . Uuid::class); + + $this->service = new ServiceOptionImporterService( + $this->connection, $this->serviceRepository, $this->repository, $this->serviceVariableRepository + ); + } + + /** + * Test that a service option can be successfully imported. + */ + public function testServiceOptionIsImported() + { + $option = factory(ServiceOption::class)->make(); + $service = factory(Service::class)->make(); + $service->options = collect([factory(ServiceOption::class)->make()]); + + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); + $this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([ + 'meta' => ['version' => 'PTDL_v1'], + 'name' => $option->name, + 'tag' => $option->tag, + 'variables' => [ + $variable = factory(ServiceVariable::class)->make(), + ], + ])); + $this->serviceRepository->shouldReceive('getWithOptions')->with($service->id)->once()->andReturn($service); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn($option->uuid); + $this->repository->shouldReceive('create')->with(m::subset([ + 'uuid' => $option->uuid, + 'service_id' => $service->id, + 'name' => $option->name, + 'tag' => $option->tag, + ]), true, true)->once()->andReturn($option); + + $this->serviceVariableRepository->shouldReceive('create')->with(m::subset([ + 'option_id' => $option->id, + 'env_variable' => $variable->env_variable, + ]))->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($this->file, $service->id); + $this->assertNotEmpty($response); + $this->assertInstanceOf(ServiceOption::class, $response); + $this->assertSame($option, $response); + } + + /** + * Test that an exception is thrown if the file is invalid. + */ + public function testExceptionIsThrownIfFileIsInvalid() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(false); + try { + $this->service->handle($this->file, 1234); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(InvalidFileUploadException::class, $exception); + $this->assertEquals(trans('exceptions.service.exporter.import_file_error'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if the file is not a file. + */ + public function testExceptionIsThrownIfFileIsNotAFile() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(false); + + try { + $this->service->handle($this->file, 1234); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(InvalidFileUploadException::class, $exception); + $this->assertEquals(trans('exceptions.service.exporter.import_file_error'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if the JSON metadata is invalid. + */ + public function testExceptionIsThrownIfJsonMetaDataIsInvalid() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); + $this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([ + 'meta' => ['version' => 'hodor'], + ])); + + try { + $this->service->handle($this->file, 1234); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(InvalidFileUploadException::class, $exception); + $this->assertEquals(trans('exceptions.service.exporter.invalid_json_provided'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if a duplicate tag exists. + */ + public function testExceptionIsThrownIfDuplicateTagExists() + { + $option = factory(ServiceOption::class)->make(); + $service = factory(Service::class)->make(); + $service->options = collect([factory(ServiceOption::class)->make(['tag' => $option->tag])]); + + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('isFile')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(100); + $this->file->shouldReceive('openFile->fread')->with(100)->once()->andReturn(json_encode([ + 'meta' => ['version' => 'PTDL_v1'], + 'tag' => $option->tag, + ])); + $this->serviceRepository->shouldReceive('getWithOptions')->with($service->id)->once()->andReturn($service); + + try { + $this->service->handle($this->file, $service->id); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DuplicateOptionTagException::class, $exception); + $this->assertEquals(trans('exceptions.service.options.duplicate_tag'), $exception->getMessage()); + } + } +} From 609bf3284386bc9947026eb9668751eaa0bcbd0d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 4 Oct 2017 23:42:04 -0500 Subject: [PATCH 13/34] Add test for service option exporter --- app/helpers.php | 28 +++++++ database/factories/ModelFactory.php | 1 + .../NestedObjectAssertionsTrait.php | 47 +++++++++++ .../ServiceOptionExporterServiceTest.php | 78 +++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 tests/Assertions/NestedObjectAssertionsTrait.php create mode 100644 tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php diff --git a/app/helpers.php b/app/helpers.php index 26dd1b8e0..0c9004695 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -43,3 +43,31 @@ if (! function_exists('is_digit')) { return is_bool($value) ? false : ctype_digit(strval($value)); } } + +if (! function_exists('object_get_strict')) { + /** + * Get an object using dot notation. An object key with a value of null is still considered valid + * and will not trigger the response of a default value (unlike object_get). + * + * @param object $object + * @param string $key + * @param null $default + * @return mixed + */ + function object_get_strict($object, $key, $default = null) + { + if (is_null($key) || trim($key) == '') { + return $object; + } + + foreach (explode('.', $key) as $segment) { + if (! is_object($object) || ! property_exists($object, $segment)) { + return value($default); + } + + $object = $object->{$segment}; + } + + return $object; + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index d61f46cc0..d8d14526b 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -107,6 +107,7 @@ $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Genera 'service_id' => $faker->unique()->randomNumber(), 'name' => $faker->name, 'description' => implode(' ', $faker->sentences(3)), + 'startup' => 'java -jar test.jar', 'tag' => 'test@testfactory.com:' . $faker->unique()->randomNumber(8), ]; }); diff --git a/tests/Assertions/NestedObjectAssertionsTrait.php b/tests/Assertions/NestedObjectAssertionsTrait.php new file mode 100644 index 000000000..b402696d8 --- /dev/null +++ b/tests/Assertions/NestedObjectAssertionsTrait.php @@ -0,0 +1,47 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Tests\Assertions; + +use PHPUnit\Framework\Assert; +use PHPUnit_Util_InvalidArgumentHelper; + +trait NestedObjectAssertionsTrait +{ + /** + * Assert that an object value matches an expected value. + * + * @param string $key + * @param mixed $expected + * @param object $object + */ + public function assertObjectNestedValueEquals(string $key, $expected, $object) + { + if (! is_object($object)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'object'); + } + + Assert::assertEquals($expected, object_get_strict($object, $key, '__TEST_FAILURE'), 'Assert that an object value equals a provided value.'); + } + + /** + * Assert that an object contains a nested key. + * + * @param string $key + * @param object $object + */ + public function assertObjectHasNestedAttribute(string $key, $object) + { + if (! is_object($object)) { + throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'object'); + } + + Assert::assertNotEquals('__TEST_FAILURE', object_get_strict($object, $key, '__TEST_FAILURE'), 'Assert that an object contains a nested key.'); + } +} diff --git a/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php b/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php new file mode 100644 index 000000000..381e1c015 --- /dev/null +++ b/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php @@ -0,0 +1,78 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Tests\Unit\Services\Services\Sharing; + +use Mockery as m; +use Carbon\Carbon; +use Tests\TestCase; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Tests\Assertions\NestedObjectAssertionsTrait; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService; + +class ServiceOptionExporterServiceTest extends TestCase +{ + use NestedObjectAssertionsTrait; + + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface|\Mockery\Mock + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Sharing\ServiceOptionExporterService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + Carbon::setTestNow(Carbon::now()); + $this->carbon = new Carbon(); + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + + $this->service = new ServiceOptionExporterService($this->carbon, $this->repository); + } + + public function testJsonStructureIsExported() + { + $option = factory(ServiceOption::class)->make(); + $option->variables = collect([$variable = factory(ServiceVariable::class)->make()]); + + $this->repository->shouldReceive('getWithExportAttributes')->with($option->id)->once()->andReturn($option); + + $response = $this->service->handle($option->id); + $this->assertNotEmpty($response); + + $data = json_decode($response); + $this->assertEquals(JSON_ERROR_NONE, json_last_error()); + $this->assertObjectHasNestedAttribute('meta.version', $data); + $this->assertObjectNestedValueEquals('meta.version', 'PTDL_v1', $data); + $this->assertObjectHasNestedAttribute('exported_at', $data); + $this->assertObjectNestedValueEquals('exported_at', Carbon::now()->toIso8601String(), $data); + $this->assertObjectHasNestedAttribute('scripts.installation.script', $data); + $this->assertObjectHasNestedAttribute('scripts.installation.container', $data); + $this->assertObjectHasNestedAttribute('scripts.installation.entrypoint', $data); + $this->assertObjectHasAttribute('variables', $data); + $this->assertArrayHasKey('0', $data->variables); + $this->assertObjectHasAttribute('name', $data->variables[0]); + $this->assertObjectNestedValueEquals('name', $variable->name, $data->variables[0]); + } +} From fbd5c25ed0b1ef0cbce4a67d2f06920ddd7e9acc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 4 Oct 2017 23:52:25 -0500 Subject: [PATCH 14/34] Finalize tests --- CHANGELOG.md | 1 + .../Services/Options/InstallScriptUpdateService.php | 2 +- app/Services/Services/Options/OptionUpdateService.php | 2 +- app/Services/Services/ServiceUpdateService.php | 2 +- .../Services/Services/Options/OptionCreationServiceTest.php | 6 ++++-- .../Services/Sharing/ServiceOptionExporterServiceTest.php | 3 +++ 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74573ba2..f70fbeb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * New CLI command to disabled 2-Factor Authentication on an account if necessary. * Ability to delete users and locations via the CLI. * You can now require 2FA for all users, admins only, or at will using a simple configuration in the Admin CP. +* Added ability to export and import service options and their associated settings and environment variables via the Admin CP. ### Changed * Theme colors and login pages updated to give a more unique feel to the project. diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index abb7cfca1..7b302190e 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -40,7 +40,7 @@ class InstallScriptUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException */ - public function handle($option, array $data): void + public function handle($option, array $data) { if (! $option instanceof ServiceOption) { $option = $this->repository->find($option); diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index 73c69cc5d..1d2109de5 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -40,7 +40,7 @@ class OptionUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException */ - public function handle($option, array $data): void + public function handle($option, array $data) { if (! $option instanceof ServiceOption) { $option = $this->repository->find($option); diff --git a/app/Services/Services/ServiceUpdateService.php b/app/Services/Services/ServiceUpdateService.php index 327c45aa7..c6f5e322f 100644 --- a/app/Services/Services/ServiceUpdateService.php +++ b/app/Services/Services/ServiceUpdateService.php @@ -36,7 +36,7 @@ class ServiceUpdateService * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle(int $service, array $data): void + public function handle(int $service, array $data) { if (! is_null(array_get($data, 'author'))) { unset($data['author']); diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php index c15db8f07..8238f858d 100644 --- a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -60,14 +60,16 @@ class OptionCreationServiceTest extends TestCase */ public function testCreateNewModelWithoutUsingConfigFrom() { - $model = factory(ServiceOption::class)->make(); + $model = factory(ServiceOption::class)->make([ + 'tag' => str_random(10), + ]); $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('test@example.com'); $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-string'); $this->repository->shouldReceive('create')->with([ 'name' => $model->name, - 'config_from' => null, 'tag' => 'test@example.com:' . $model->tag, + 'config_from' => null, 'uuid' => 'uuid-string', ], true, true)->once()->andReturn($model); diff --git a/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php b/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php index 381e1c015..aa5e35737 100644 --- a/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php +++ b/tests/Unit/Services/Services/Sharing/ServiceOptionExporterServiceTest.php @@ -51,6 +51,9 @@ class ServiceOptionExporterServiceTest extends TestCase $this->service = new ServiceOptionExporterService($this->carbon, $this->repository); } + /** + * Test that a JSON structure is returned. + */ public function testJsonStructureIsExported() { $option = factory(ServiceOption::class)->make(); From 366221fa3f1f92e9955333820c8802cb4e0c296f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 5 Oct 2017 20:13:01 -0500 Subject: [PATCH 15/34] ; --- resources/themes/pterodactyl/layouts/admin.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index c547e2762..115f952fa 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -200,7 +200,7 @@ @show From 38075c6b9fde2e7234094ab4a1b0e4d124cda7f3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 5 Oct 2017 23:09:43 -0500 Subject: [PATCH 16/34] Update panel to send correct service option configuration to the daemon. --- .../Commands/Server/RebuildServerCommand.php | 32 +++---- .../Daemon/ServerRepositoryInterface.php | 9 +- .../ServiceOptionRepositoryInterface.php | 15 +++- .../API/Remote/OptionRetrievalController.php | 74 +++++++++++++++++ app/Repositories/Daemon/ServerRepository.php | 54 +++--------- .../Eloquent/ServerRepository.php | 6 +- .../Eloquent/ServiceOptionRepository.php | 21 ++++- .../ServerConfigurationStructureService.php | 83 +++++++++++++++++++ .../Servers/ServerCreationService.php | 11 ++- .../OptionConfigurationFileService.php | 51 ++++++++++++ routes/api-remote.php | 7 +- routes/daemon.php | 2 - 12 files changed, 282 insertions(+), 83 deletions(-) create mode 100644 app/Http/Controllers/API/Remote/OptionRetrievalController.php create mode 100644 app/Services/Servers/ServerConfigurationStructureService.php create mode 100644 app/Services/Services/Options/OptionConfigurationFileService.php diff --git a/app/Console/Commands/Server/RebuildServerCommand.php b/app/Console/Commands/Server/RebuildServerCommand.php index 562b10bd9..c6b562b06 100644 --- a/app/Console/Commands/Server/RebuildServerCommand.php +++ b/app/Console/Commands/Server/RebuildServerCommand.php @@ -12,12 +12,17 @@ namespace Pterodactyl\Console\Commands\Server; use Webmozart\Assert\Assert; use Illuminate\Console\Command; use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class RebuildServerCommand extends Command { + /** + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + protected $configurationStructureService; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ @@ -28,11 +33,6 @@ class RebuildServerCommand extends Command */ protected $description = 'Rebuild a single server, all servers on a node, or all servers on the panel.'; - /** - * @var \Pterodactyl\Services\Servers\EnvironmentService - */ - protected $environmentService; - /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ @@ -49,18 +49,18 @@ class RebuildServerCommand extends Command * RebuildServerCommand constructor. * * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository - * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService + * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ public function __construct( DaemonServerRepositoryInterface $daemonRepository, - EnvironmentService $environmentService, + ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository ) { parent::__construct(); + $this->configurationStructureService = $configurationStructureService; $this->daemonRepository = $daemonRepository; - $this->environmentService = $environmentService; $this->repository = $repository; } @@ -74,19 +74,7 @@ class RebuildServerCommand extends Command $servers->each(function ($server) use ($bar) { $bar->clear(); - $json = [ - 'build' => [ - 'image' => $server->image, - 'env|overwrite' => $this->environmentService->process($server), - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => object_get($server, 'pack.uuid'), - 'skip_scripts' => $server->skip_scripts, - ], - 'rebuild' => true, - ]; + $json = array_merge($this->configurationStructureService->handle($server), ['rebuild' => true]); try { $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($json); diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index c64853b8c..6b7a86d45 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -9,17 +9,20 @@ namespace Pterodactyl\Contracts\Repository\Daemon; +use Psr\Http\Message\ResponseInterface; + interface ServerRepositoryInterface extends BaseRepositoryInterface { /** * Create a new server on the daemon for the panel. * - * @param int $id + * @param array $structure * @param array $overrides - * @param bool $start * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ - public function create($id, array $overrides = [], $start = false); + public function create(array $structure, array $overrides = []): ResponseInterface; /** * Update server details on the daemon. diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index c7acd5a45..310b385ea 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Contracts\Repository; use Pterodactyl\Models\ServiceOption; +use Illuminate\Database\Eloquent\Collection; interface ServiceOptionRepositoryInterface extends RepositoryInterface { @@ -23,15 +24,21 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface */ public function getWithVariables(int $id): ServiceOption; + /** + * Return all of the service options and their relations to be used in the daemon API. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllWithCopyAttributes(): Collection; + /** * Return a service option with the scriptFrom and configFrom relations loaded onto the model. * - * @param int $id + * @param int|string $value + * @param string $column * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyAttributes(int $id): ServiceOption; + public function getWithCopyAttributes($value, string $column = 'id'): ServiceOption; /** * Return all of the data needed to export a service. diff --git a/app/Http/Controllers/API/Remote/OptionRetrievalController.php b/app/Http/Controllers/API/Remote/OptionRetrievalController.php new file mode 100644 index 000000000..fdd715ca9 --- /dev/null +++ b/app/Http/Controllers/API/Remote/OptionRetrievalController.php @@ -0,0 +1,74 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Http\Controllers\API\Remote; + +use Illuminate\Http\JsonResponse; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Services\Services\Options\OptionConfigurationFileService; + +class OptionRetrievalController extends Controller +{ + /** + * @var \Pterodactyl\Services\Services\Options\OptionConfigurationFileService + */ + protected $configurationFileService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * OptionUpdateController constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + * @param \Pterodactyl\Services\Services\Options\OptionConfigurationFileService $configurationFileService + */ + public function __construct( + ServiceOptionRepositoryInterface $repository, + OptionConfigurationFileService $configurationFileService + ) { + $this->configurationFileService = $configurationFileService; + $this->repository = $repository; + } + + /** + * Return a JSON array of service options and the SHA1 hash of thier configuration file. + * + * @return \Illuminate\Http\JsonResponse + */ + public function index(): JsonResponse + { + $options = $this->repository->getAllWithCopyAttributes(); + + $response = []; + $options->each(function ($option) use (&$response) { + $response[$option->uuid] = sha1(json_encode($this->configurationFileService->handle($option))); + }); + + return response()->json($response); + } + + /** + * Return the configuration file for a single service option for the Daemon. + * + * @param string $uuid + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function download(string $uuid): JsonResponse + { + $option = $this->repository->getWithCopyAttributes($uuid, 'uuid'); + + return response()->json($this->configurationFileService->handle($option)); + } +} diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index c1690eb1a..3515b26e4 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -10,61 +10,29 @@ namespace Pterodactyl\Repositories\Daemon; use Webmozart\Assert\Assert; -use Pterodactyl\Services\Servers\EnvironmentService; +use Psr\Http\Message\ResponseInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { /** - * {@inheritdoc} + * Create a new server on the daemon for the panel. + * + * @param array $structure + * @param array $overrides + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ - public function create($id, array $overrides = [], $start = false) + public function create(array $structure, array $overrides = []): ResponseInterface { - Assert::numeric($id, 'First argument passed to create must be numeric, received %s.'); - Assert::boolean($start, 'Third argument passed to create must be boolean, received %s.'); - - $repository = $this->app->make(DatabaseServerRepositoryInterface::class); - $environment = $this->app->make(EnvironmentService::class); - - $server = $repository->getDataForCreation($id); - - $data = [ - 'uuid' => (string) $server->uuid, - 'user' => $server->username, - 'build' => [ - 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, - ], - 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - 'env' => $environment->process($server), - 'memory' => (int) $server->memory, - 'swap' => (int) $server->swap, - 'io' => (int) $server->io, - 'cpu' => (int) $server->cpu, - 'disk' => (int) $server->disk, - 'image' => $server->image, - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => object_get($server, 'pack.uuid'), - 'skip_scripts' => $server->skip_scripts, - ], - 'rebuild' => false, - 'start_on_completion' => $start, - ]; - // Loop through overrides. foreach ($overrides as $key => $value) { - array_set($data, $key, $value); + array_set($structure, $key, $value); } return $this->getHttpClient()->request('POST', 'servers', [ - 'json' => $data, + 'json' => $structure, ]); } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 5c86e4a89..2443f6b25 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -47,7 +47,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt Assert::nullOrIntegerish($server, 'First argument passed to getDataForRebuild must be null or integer, received %s.'); Assert::nullOrIntegerish($node, 'Second argument passed to getDataForRebuild must be null or integer, received %s.'); - $instance = $this->getBuilder()->with('node', 'option.service', 'pack'); + $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option', 'node'); if (! is_null($server) && is_null($node)) { $instance = $instance->where('id', '=', $server); @@ -111,9 +111,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt */ public function getDataForCreation($id) { - $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option.service') - ->find($id, $this->getColumns()); - + $instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'option'])->find($id, $this->getColumns()); if (! $instance) { throw new RecordNotFoundException(); } diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index 5d8bec31d..852726e52 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -9,7 +9,9 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\ServiceOption; +use Illuminate\Database\Eloquent\Collection; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; @@ -42,18 +44,31 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio return $instance; } + /** + * Return all of the service options and their relations to be used in the daemon API. + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllWithCopyAttributes(): Collection + { + return $this->getBuilder()->with('scriptFrom', 'configFrom')->get($this->getColumns()); + } + /** * Return a service option with the scriptFrom and configFrom relations loaded onto the model. * - * @param int $id + * @param int|string $value + * @param string $column * @return \Pterodactyl\Models\ServiceOption * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getWithCopyAttributes(int $id): ServiceOption + public function getWithCopyAttributes($value, string $column = 'id'): ServiceOption { + Assert::true((is_digit($value) || is_string($value)), 'First argument passed to getWithCopyAttributes must be an integer or string, received %s.'); + /** @var \Pterodactyl\Models\ServiceOption $instance */ - $instance = $this->getBuilder()->with('scriptFrom', 'configFrom')->find($id, $this->getColumns()); + $instance = $this->getBuilder()->with('scriptFrom', 'configFrom')->where($column, '=', $value)->first($this->getColumns()); if (! $instance) { throw new RecordNotFoundException; } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php new file mode 100644 index 000000000..78fee994a --- /dev/null +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -0,0 +1,83 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Servers; + +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class ServerConfigurationStructureService +{ + const REQUIRED_RELATIONS = ['allocation', 'allocations', 'pack', 'option']; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $environment; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * ServerConfigurationStructureService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Services\Servers\EnvironmentService $environment + */ + public function __construct( + ServerRepositoryInterface $repository, + EnvironmentService $environment + ) { + $this->repository = $repository; + $this->environment = $environment; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * @return array + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($server): array + { + if (! $server instanceof Server || array_diff(self::REQUIRED_RELATIONS, $server->getRelations())) { + $server = $this->repository->getDataForCreation(is_digit($server) ? $server : $server->id); + } + + return [ + 'uuid' => $server->uuid, + 'user' => $server->username, + 'build' => [ + 'default' => [ + 'ip' => $server->allocation->ip, + 'port' => $server->allocation->port, + ], + 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(), + 'env' => $this->environment->process($server), + 'memory' => (int) $server->memory, + 'swap' => (int) $server->swap, + 'io' => (int) $server->io, + 'cpu' => (int) $server->cpu, + 'disk' => (int) $server->disk, + 'image' => $server->image, + ], + 'keys' => [], + 'service' => [ + 'option' => $server->option->uuid, + 'pack' => object_get($server, 'pack.uuid'), + 'skip_scripts' => $server->skip_scripts, + ], + 'rebuild' => false, + 'suspended' => (int) $server->suspended, + ]; + } +} diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 122bac629..6ddf4496a 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -29,6 +29,11 @@ class ServerCreationService */ protected $allocationRepository; + /** + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService + */ + protected $configurationStructureService; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ @@ -81,6 +86,7 @@ class ServerCreationService * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository @@ -93,6 +99,7 @@ class ServerCreationService DaemonServerRepositoryInterface $daemonServerRepository, DatabaseManager $database, NodeRepositoryInterface $nodeRepository, + ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, @@ -102,6 +109,7 @@ class ServerCreationService ) { $this->allocationRepository = $allocationRepository; $this->daemonServerRepository = $daemonServerRepository; + $this->configurationStructureService = $configurationStructureService; $this->database = $database; $this->nodeRepository = $nodeRepository; $this->repository = $repository; @@ -175,10 +183,11 @@ class ServerCreationService } $this->serverVariableRepository->insert($records); + $structure = $this->configurationStructureService->handle($server->id); // Create the server on the daemon & commit it to the database. try { - $this->daemonServerRepository->setNode($server->node_id)->create($server->id); + $this->daemonServerRepository->setNode($server->node_id)->create($structure, ['start_on_completion' => (bool) $data['start_on_completion']]); $this->database->commit(); } catch (RequestException $exception) { $response = $exception->getResponse(); diff --git a/app/Services/Services/Options/OptionConfigurationFileService.php b/app/Services/Services/Options/OptionConfigurationFileService.php new file mode 100644 index 000000000..c21f435dd --- /dev/null +++ b/app/Services/Services/Options/OptionConfigurationFileService.php @@ -0,0 +1,51 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Services\Services\Options; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class OptionConfigurationFileService +{ + protected $repository; + + /** + * OptionConfigurationFileService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct(ServiceOptionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Return a service configuration file to be used by the daemon. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($option): array + { + if (! $option instanceof ServiceOption) { + $option = $this->repository->getWithCopyAttributes($option); + } + + return [ + 'startup' => json_decode($option->inherit_config_startup), + 'stop' => $option->inherit_config_stop, + 'configs' => json_decode($option->inherit_config_files), + 'log' => json_decode($option->inherit_config_logs), + 'query' => 'none', + ]; + } +} diff --git a/routes/api-remote.php b/routes/api-remote.php index 54e5d8da4..39a371b2e 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -6,4 +6,9 @@ * This software is licensed under the terms of the MIT license. * https://opensource.org/licenses/MIT */ -Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('post.api.remote.authenticate'); +Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('api.remote.authenticate'); + +Route::group(['prefix' => '/options'], function () { + Route::get('/', 'OptionRetrievalController@index')->name('api.remote.services'); + Route::get('/{uuid}', 'OptionRetrievalController@download')->name('api.remote.services.download'); +}); diff --git a/routes/daemon.php b/routes/daemon.php index e6d34e971..96dd4e682 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -6,8 +6,6 @@ * This software is licensed under the terms of the MIT license. * https://opensource.org/licenses/MIT */ -Route::get('/services', 'ServiceController@listServices')->name('daemon.services'); -Route::get('/services/pull/{service}/{file}', 'ServiceController@pull')->name('daemon.pull'); Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull'); Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); Route::get('/details/option/{server}', 'OptionController@details')->name('daemon.option.details'); From 675e780946fd5853e82678cde049e9bd801f7a1e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 6 Oct 2017 00:16:22 -0500 Subject: [PATCH 17/34] Fix test failures --- app/Exceptions/DisplayException.php | 5 +- .../Connection/DaemonConnectionException.php | 31 +++++ .../Servers/ServerCreationService.php | 50 +++----- .../Servers/ServerCreationServiceTest.php | 117 +++++++----------- 4 files changed, 100 insertions(+), 103 deletions(-) create mode 100644 app/Exceptions/Http/Connection/DaemonConnectionException.php diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index 72fb86c71..80c5771a5 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -14,6 +14,9 @@ use Throwable; class DisplayException extends PterodactylException { + const LEVEL_WARNING = 'warning'; + const LEVEL_ERROR = 'error'; + /** * @var string */ @@ -27,7 +30,7 @@ class DisplayException extends PterodactylException * @param string $level * @internal param mixed $log */ - public function __construct($message, Throwable $previous = null, $level = 'error') + public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR) { $this->level = $level; diff --git a/app/Exceptions/Http/Connection/DaemonConnectionException.php b/app/Exceptions/Http/Connection/DaemonConnectionException.php new file mode 100644 index 000000000..5718af63f --- /dev/null +++ b/app/Exceptions/Http/Connection/DaemonConnectionException.php @@ -0,0 +1,31 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Exceptions\Http\Connection; + +use GuzzleHttp\Exception\GuzzleException; +use Pterodactyl\Exceptions\DisplayException; + +class DaemonConnectionException extends DisplayException +{ + /** + * Throw a displayable exception caused by a daemon connection error. + * + * @param \GuzzleHttp\Exception\GuzzleException $previous + */ + public function __construct(GuzzleException $previous) + { + /** @var \GuzzleHttp\Psr7\Response|null $response */ + $response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null; + + parent::__construct(trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ]), $previous, DisplayException::LEVEL_WARNING); + } +} diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 6ddf4496a..1a8f63006 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -10,15 +10,14 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; -use Illuminate\Log\Writer; -use Illuminate\Database\DatabaseManager; use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -34,16 +33,16 @@ class ServerCreationService */ protected $configurationStructureService; + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ protected $daemonServerRepository; - /** - * @var \Illuminate\Database\DatabaseManager - */ - protected $database; - /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ @@ -74,17 +73,12 @@ class ServerCreationService */ protected $validatorService; - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; - /** * CreationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository - * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository @@ -92,32 +86,29 @@ class ServerCreationService * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService - * @param \Illuminate\Log\Writer $writer */ public function __construct( AllocationRepositoryInterface $allocationRepository, + ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, - DatabaseManager $database, NodeRepositoryInterface $nodeRepository, ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, UsernameGenerationService $usernameService, - VariableValidatorService $validatorService, - Writer $writer + VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; - $this->daemonServerRepository = $daemonServerRepository; $this->configurationStructureService = $configurationStructureService; - $this->database = $database; + $this->connection = $connection; + $this->daemonServerRepository = $daemonServerRepository; $this->nodeRepository = $nodeRepository; $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; $this->usernameService = $usernameService; $this->validatorService = $validatorService; - $this->writer = $writer; } /** @@ -136,7 +127,7 @@ class ServerCreationService $validator = $this->validatorService->isAdmin()->setFields($data['environment'])->validate($data['option_id']); $uniqueShort = str_random(8); - $this->database->beginTransaction(); + $this->connection->beginTransaction(); $server = $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), @@ -187,16 +178,13 @@ class ServerCreationService // Create the server on the daemon & commit it to the database. try { - $this->daemonServerRepository->setNode($server->node_id)->create($structure, ['start_on_completion' => (bool) $data['start_on_completion']]); - $this->database->commit(); + $this->daemonServerRepository->setNode($server->node_id)->create($structure, [ + 'start_on_completion' => (bool) array_get($data, 'start_on_completion', false), + ]); + $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - $this->writer->warning($exception); - $this->database->rollBack(); - - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); } return $server; diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 5244dfe3b..0b22486e5 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -9,14 +9,12 @@ namespace Tests\Unit\Services\Servers; -use Exception; use Mockery as m; use Tests\TestCase; -use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; -use Illuminate\Database\DatabaseManager; use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Servers\UsernameGenerationService; @@ -24,20 +22,35 @@ use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; +use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +/** + * @preserveGlobalState disabled + */ class ServerCreationServiceTest extends TestCase { use PHPMock; /** - * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock */ protected $allocationRepository; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock + */ + protected $configurationStructureService; + + /** + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ protected $daemonServerRepository; @@ -66,27 +79,22 @@ class ServerCreationServiceTest extends TestCase ]; /** - * @var \Illuminate\Database\DatabaseManager - */ - protected $database; - - /** - * @var \GuzzleHttp\Exception\RequestException + * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock */ protected $exception; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock */ protected $nodeRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ protected $repository; /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock */ protected $serverVariableRepository; @@ -96,30 +104,25 @@ class ServerCreationServiceTest extends TestCase protected $service; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ protected $userRepository; /** - * @var \Pterodactyl\Services\Servers\UsernameGenerationService + * @var \Pterodactyl\Services\Servers\UsernameGenerationService|\Mockery\Mock */ protected $usernameService; /** - * @var \Pterodactyl\Services\Servers\VariableValidatorService + * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock */ protected $validatorService; /** - * @var \Ramsey\Uuid\Uuid + * @var \Ramsey\Uuid\Uuid|\Mockery\Mock */ protected $uuid; - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; - /** * Setup tests. */ @@ -128,8 +131,9 @@ class ServerCreationServiceTest extends TestCase parent::setUp(); $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); + $this->configurationStructureService = m::mock(ServerConfigurationStructureService::class); + $this->connection = m::mock(ConnectionInterface::class); $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->database = m::mock(DatabaseManager::class); $this->exception = m::mock(RequestException::class); $this->nodeRepository = m::mock(NodeRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class); @@ -138,25 +142,21 @@ class ServerCreationServiceTest extends TestCase $this->usernameService = m::mock(UsernameGenerationService::class); $this->validatorService = m::mock(VariableValidatorService::class); $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); - $this->writer = m::mock(Writer::class); $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') ->expects($this->any())->willReturn('random_string'); - $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') - ->expects($this->any())->willReturn('s'); - $this->service = new ServerCreationService( $this->allocationRepository, + $this->connection, $this->daemonServerRepository, - $this->database, $this->nodeRepository, + $this->configurationStructureService, $this->repository, $this->serverVariableRepository, $this->userRepository, $this->usernameService, - $this->validatorService, - $this->writer + $this->validatorService ); } @@ -169,37 +169,12 @@ class ServerCreationServiceTest extends TestCase ->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf() ->shouldReceive('validate')->with($this->data['option_id'])->once()->andReturnSelf(); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('toString')->withNoArgs()->once()->andReturn('uuid-0000'); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-0000'); $this->usernameService->shouldReceive('generate')->with($this->data['name'], 'random_string') ->once()->andReturn('user_name'); - $this->repository->shouldReceive('create')->with([ - 'uuid' => 'uuid-0000', - 'uuidShort' => 'random_string', - 'node_id' => $this->data['node_id'], - 'name' => $this->data['name'], - 'description' => $this->data['description'], - 'skip_scripts' => false, - 'suspended' => false, - 'owner_id' => $this->data['owner_id'], - 'memory' => $this->data['memory'], - 'swap' => $this->data['swap'], - 'disk' => $this->data['disk'], - 'io' => $this->data['io'], - 'cpu' => $this->data['cpu'], - 'oom_disabled' => false, - 'allocation_id' => $this->data['allocation_id'], - 'service_id' => $this->data['service_id'], - 'option_id' => $this->data['option_id'], - 'pack_id' => null, - 'startup' => $this->data['startup'], - 'daemonSecret' => 'random_string', - 'image' => $this->data['docker_image'], - 'username' => 'user_name', - 'sftp_password' => null, - ])->once()->andReturn((object) [ + $this->repository->shouldReceive('create')->withAnyArgs()->once()->andReturn((object) [ 'node_id' => 1, 'id' => 1, ]); @@ -216,9 +191,12 @@ class ServerCreationServiceTest extends TestCase 'variable_id' => 1, 'variable_value' => 'var1-value', ]])->once()->andReturnNull(); + + $this->configurationStructureService->shouldReceive('handle')->with(1)->once()->andReturn(['test' => 'struct']); + $this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf() - ->shouldReceive('create')->with(1)->once()->andReturnNull(); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + ->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->service->create($this->data); @@ -232,9 +210,9 @@ class ServerCreationServiceTest extends TestCase public function testExceptionShouldBeThrownIfTheRequestFails() { $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->uuid->shouldReceive('uuid4->toString')->once()->andReturn('uuid-0000'); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); + $this->uuid->shouldReceive('uuid4->toString')->withNoArgs()->once()->andReturn('uuid-0000'); $this->repository->shouldReceive('create')->once()->andReturn((object) [ 'node_id' => 1, 'id' => 1, @@ -242,18 +220,15 @@ class ServerCreationServiceTest extends TestCase $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); $this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull(); + $this->configurationStructureService->shouldReceive('handle')->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception); $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); - $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); - $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); try { $this->service->create($this->data); - } catch (Exception $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ - 'code' => 'E_CONN_REFUSED', - ]), $exception->getMessage()); + } catch (PterodactylException $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); } } } From 0b3c0f6d5a91e8625642f362afda26e12d09a255 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 6 Oct 2017 20:39:11 -0500 Subject: [PATCH 18/34] Ah yes, lets just spend 30 minutes trying to get a migration to run correctly. --- ...10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php | 5 +++-- ...0_02_202007_ChangeToABetterUniqueServiceConfiguration.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php index 64d31f749..d7e9caa86 100644 --- a/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php +++ b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php @@ -41,10 +41,11 @@ class ChangeServicesToUseAMoreUniqueIdentifier extends Migration { Schema::table('services', function (Blueprint $table) { $table->dropColumn('uuid'); - $table->string('folder')->unique('file'); - $table->char('author', 36)->change(); + $table->string('folder')->nullable(); + $table->string('author', 36)->change(); $table->unique('name'); + $table->unique('folder', 'services_file_unique'); }); } } diff --git a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php index 990a07a63..96a968144 100644 --- a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php +++ b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php @@ -49,7 +49,7 @@ class ChangeToABetterUniqueServiceConfiguration extends Migration }); DB::transaction(function () { - DB::table('service_options')->select(['id', 'author'])->get()->each(function ($option) { + DB::table('service_options')->select(['id', 'tag'])->get()->each(function ($option) { DB::table('service_options')->where('id', $option->id)->update([ 'tag' => array_get(explode(':', $option->tag), 1), ]); From 344c1a988535a94d69a4bd3098c6dd069c55c274 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 6 Oct 2017 21:22:32 -0500 Subject: [PATCH 19/34] First push before :egg: --- app/Models/Service.php | 6 -- app/Models/ServiceOption.php | 11 --- .../Sharing/ServiceOptionExporterService.php | 2 - database/factories/ModelFactory.php | 7 +- ...22_RemoveDaemonSecretFromSubusersTable.php | 6 +- ...angeServicesToUseAMoreUniqueIdentifier.php | 4 + ...ngeToABetterUniqueServiceConfiguration.php | 9 +-- .../admin/services/functions.blade.php | 73 ------------------- .../pterodactyl/admin/services/new.blade.php | 15 +--- .../pterodactyl/admin/services/view.blade.php | 59 ++++++--------- routes/admin.php | 2 - 11 files changed, 36 insertions(+), 158 deletions(-) delete mode 100644 resources/themes/pterodactyl/admin/services/functions.blade.php diff --git a/app/Models/Service.php b/app/Models/Service.php index 0a17f7e42..e6c69bede 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -34,8 +34,6 @@ class Service extends Model implements CleansAttributes, ValidableContract protected $fillable = [ 'name', 'description', - 'startup', - 'index_file', ]; /** @@ -45,8 +43,6 @@ class Service extends Model implements CleansAttributes, ValidableContract 'author' => 'required', 'name' => 'required', 'description' => 'sometimes', - 'startup' => 'sometimes', - 'index_file' => 'required', ]; /** @@ -56,8 +52,6 @@ class Service extends Model implements CleansAttributes, ValidableContract 'author' => 'email', 'name' => 'string|max:255', 'description' => 'nullable|string', - 'startup' => 'nullable|string', - 'index_file' => 'string', ]; /** diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index f4bc72eaa..e424227fc 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -107,17 +107,6 @@ class ServiceOption extends Model implements CleansAttributes, ValidableContract 'docker_image' => null, ]; - /** - * Returns the display startup string for the option and will use the parent - * service one if the option does not have one defined. - * - * @return string - */ - public function getDisplayStartupAttribute() - { - return (is_null($this->startup)) ? $this->service->startup : $this->startup; - } - /** * Returns the install script for the option; if option is copying from another * it will return the copied script. diff --git a/app/Services/Services/Sharing/ServiceOptionExporterService.php b/app/Services/Services/Sharing/ServiceOptionExporterService.php index 744d298a4..8f2d1ebf5 100644 --- a/app/Services/Services/Sharing/ServiceOptionExporterService.php +++ b/app/Services/Services/Sharing/ServiceOptionExporterService.php @@ -58,10 +58,8 @@ class ServiceOptionExporterService 'exported_at' => $this->carbon->now()->toIso8601String(), 'name' => $option->name, 'author' => array_get(explode(':', $option->tag), 0), - 'tag' => $option->tag, 'description' => $option->description, 'image' => $option->docker_image, - 'startup' => $option->display_startup, 'config' => [ 'files' => $option->inherit_config_files, 'startup' => $option->inherit_config_startup, diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index d8d14526b..46895d70d 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -91,12 +91,10 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake $factory->define(Pterodactyl\Models\Service::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), - 'author' => $faker->unique()->uuid, + 'uuid' => $faker->unique()->uuid, + 'author' => 'testauthor@example.com', 'name' => $faker->word, 'description' => null, - 'folder' => strtolower($faker->unique()->word), - 'startup' => 'java -jar test.jar', - 'index_file' => 'indexjs', ]; }); @@ -108,7 +106,6 @@ $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Genera 'name' => $faker->name, 'description' => implode(' ', $faker->sentences(3)), 'startup' => 'java -jar test.jar', - 'tag' => 'test@testfactory.com:' . $faker->unique()->randomNumber(8), ]; }); diff --git a/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php index a0c5e6d10..d4d2dd695 100644 --- a/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php +++ b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php @@ -42,12 +42,16 @@ class RemoveDaemonSecretFromSubusersTable extends Migration public function down() { Schema::table('subusers', function (Blueprint $table) { - $table->char('daemonSecret', 36)->after('server_id')->unique(); + $table->char('daemonSecret', 36)->after('server_id'); }); $subusers = DB::table('subusers')->get(); $subusers->each(function ($subuser) { DB::table('daemon_keys')->where('user_id', $subuser->user_id)->where('server_id', $subuser->server_id)->delete(); }); + + Schema::table('subusers', function (Blueprint $table) { + $table->unique('daemonSecret'); + }); } } diff --git a/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php index d7e9caa86..6bb36813d 100644 --- a/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php +++ b/database/migrations/2017_10_02_202000_ChangeServicesToUseAMoreUniqueIdentifier.php @@ -20,6 +20,8 @@ class ChangeServicesToUseAMoreUniqueIdentifier extends Migration $table->string('author')->change(); $table->char('uuid', 36)->after('id'); $table->dropColumn('folder'); + $table->dropColumn('startup'); + $table->dropColumn('index_file'); }); DB::table('services')->get(['id', 'author', 'uuid'])->each(function ($service) { @@ -42,6 +44,8 @@ class ChangeServicesToUseAMoreUniqueIdentifier extends Migration Schema::table('services', function (Blueprint $table) { $table->dropColumn('uuid'); $table->string('folder')->nullable(); + $table->text('startup')->nullable(); + $table->text('index_file'); $table->string('author', 36)->change(); $table->unique('name'); diff --git a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php index 96a968144..7ce854c56 100644 --- a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php +++ b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php @@ -15,19 +15,16 @@ class ChangeToABetterUniqueServiceConfiguration extends Migration { Schema::table('service_options', function (Blueprint $table) { $table->char('uuid', 36)->after('id'); - - $table->index(['service_id', 'tag']); + $table->dropColumn('tag'); }); DB::transaction(function () { DB::table('service_options')->select([ 'service_options.id', 'service_options.uuid', - 'service_options.tag', 'services.author AS service_author', ])->join('services', 'services.id', '=', 'service_options.service_id')->get()->each(function ($option) { DB::table('service_options')->where('id', $option->id)->update([ - 'tag' => $option->service_author . ':' . $option->tag, 'uuid' => Uuid::uuid4()->toString(), ]); }); @@ -45,13 +42,13 @@ class ChangeToABetterUniqueServiceConfiguration extends Migration { Schema::table('service_options', function (Blueprint $table) { $table->dropColumn('uuid'); - $table->dropIndex(['service_id', 'tag']); + $table->string('tag'); }); DB::transaction(function () { DB::table('service_options')->select(['id', 'tag'])->get()->each(function ($option) { DB::table('service_options')->where('id', $option->id)->update([ - 'tag' => array_get(explode(':', $option->tag), 1), + 'tag' => str_random(10), ]); }); }); diff --git a/resources/themes/pterodactyl/admin/services/functions.blade.php b/resources/themes/pterodactyl/admin/services/functions.blade.php deleted file mode 100644 index 12ba8af3a..000000000 --- a/resources/themes/pterodactyl/admin/services/functions.blade.php +++ /dev/null @@ -1,73 +0,0 @@ -{{-- Pterodactyl - Panel --}} -{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} - -{{-- This software is licensed under the terms of the MIT license. --}} -{{-- https://opensource.org/licenses/MIT --}} -@extends('layouts.admin') - -@section('title') - Service → {{ $service->name }} → Functions -@endsection - -@section('content-header') -

{{ $service->name }}Extend the default daemon functions using this service file.

- -@endsection - -@section('content') -
-
- -
-
-
-
-
-
-

Functions Control

-
-
-
-
{{ $service->index_file }}
- -
- - -
-
-
-@endsection - -@section('footer-scripts') - @parent - {!! Theme::js('vendor/ace/ace.js') !!} - {!! Theme::js('vendor/ace/ext-modelist.js') !!} - -@endsection diff --git a/resources/themes/pterodactyl/admin/services/new.blade.php b/resources/themes/pterodactyl/admin/services/new.blade.php index c63bec43b..cb48991ec 100644 --- a/resources/themes/pterodactyl/admin/services/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/new.blade.php @@ -21,7 +21,7 @@ @section('content')
-
+

New Service

@@ -41,19 +41,6 @@
-
-
-
-
-
-
- -
- -

The default start command to use when running options under this service. This command can be modified per-option and should include the executable to be called in the container.

-
-
-
- + - @foreach($services as $service) + @foreach($nests as $nest) - - - - - + + + + + @endforeach
NameName Description Tag Servers
{{ $option->name }}{!! $option->description !!}{!! $option->description !!} {{ $option->tag }} {{ $option->servers->count() }}
Name DescriptionOptionsEggs Packs Servers
{{ $service->name }}{{ $service->description }}{{ $service->options_count }}{{ $service->packs_count }}{{ $service->servers_count }}{{ $nest->name }}{{ $nest->description }}{{ $nest->eggs_count }}{{ $nest->packs_count }}{{ $nest->servers_count }}
@@ -63,26 +63,26 @@