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 @@