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); + }; + } }