Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions app/Http/Controllers/Admin/Nests/EggShareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
use Pterodactyl\Services\Eggs\Sharing\EggExporterService;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest;
use Pterodactyl\Http\Requests\Admin\Egg\EggImportUrlFormRequest;
use Pterodactyl\Services\Eggs\Sharing\EggUpdateImporterService;
use Pterodactyl\Exceptions\Model\InvalidFileUploadException;
use Exception;

class EggShareController extends Controller
{
Expand All @@ -22,8 +25,7 @@ public function __construct(
protected EggExporterService $exporterService,
protected EggImporterService $importerService,
protected EggUpdateImporterService $updateImporterService,
) {
}
) {}

/**
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
Expand Down Expand Up @@ -56,6 +58,48 @@ public function import(EggImportFormRequest $request): RedirectResponse
return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]);
}

/**
* Import a new service option from a URL.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function importFromUrl(EggImportUrlFormRequest $request): RedirectResponse
{
try {
$allowed_hosts = array_map(function ($item) {
return trim($item);
}, explode(',', env('ALLOWED_EGG_HOSTS', '')));
$parsed_url = parse_url($request->input('import_file_url'));

if (!is_array($parsed_url) || !isset($parsed_url['host']) || !in_array($parsed_url['host'], $allowed_hosts)) {
$this->alert->danger('The Egg import URL is not from an allowed host.')->flash();
return redirect()->back();
}
if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], ['http', 'https'])) {
$this->alert->danger('The Egg import URL scheme is invalid.')->flash();
return redirect()->back();
}

$response = @file_get_contents($request->input('import_file_url'));

if ($response === false) {
$this->alert->danger('Fetching the Egg from the URL failed.')->flash();
return redirect()->back();
}

$egg = $this->importerService->handleFromString($response, $request->input('import_to_nest'));
$this->alert->success(trans('admin/nests.eggs.notices.imported'))->flash();

return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]);
} catch (\Throwable $e) {
$this->alert->danger($e->getMessage());
return redirect()->back();
}
}

/**
* Update an existing Egg using a new imported file.
*
Expand Down
21 changes: 21 additions & 0 deletions app/Http/Requests/Admin/Egg/EggImportUrlFormRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Pterodactyl\Http\Requests\Admin\Egg;

use Pterodactyl\Http\Requests\Admin\AdminFormRequest;

class EggImportUrlFormRequest extends AdminFormRequest
{
public function rules(): array
{
$rules = [
'import_file_url' => 'bail|required|string|max:300',
];

if ($this->method() !== 'PUT') {
$rules['import_to_nest'] = 'bail|required|integer|exists:nests,id';
}

return $rules;
}
}
2 changes: 1 addition & 1 deletion app/Services/Eggs/EggParserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function fillFromParsed(Egg $model, array $parsed): Egg
* the "docker_images" field potentially not being present, and not being in the
* expected "key => value" format.
*/
protected function convertToV2(array $parsed): array
public function convertToV2(array $parsed): array
{
if (Arr::get($parsed, 'meta.version') === Egg::EXPORT_VERSION) {
return $parsed;
Expand Down
37 changes: 37 additions & 0 deletions app/Services/Eggs/Sharing/EggImporterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,41 @@ public function handle(UploadedFile $file, int $nest): Egg
return $egg;
});
}

/**
* Take a JSON string and parse it into a new egg.
*
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function handleFromString(string $json_string, int $nest): Egg
{
/** @var array $parsed */
$decoded = json_decode($json_string, true, 512, JSON_THROW_ON_ERROR);
if (!in_array(Arr::get($decoded, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) {
throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.');
}

$parsed = $this->parser->convertToV2($decoded);

/** @var Nest $nest */
$nest = Nest::query()->with('eggs', 'eggs.variables')->findOrFail($nest);

return $this->connection->transaction(function () use ($nest, $parsed) {
$egg = (new Egg())->forceFill([
'uuid' => Uuid::uuid4()->toString(),
'nest_id' => $nest->id,
'author' => Arr::get($parsed, 'author'),
'copy_script_from' => null,
]);

$egg = $this->parser->fillFromParsed($egg, $parsed);
$egg->save();

foreach ($parsed['variables'] ?? [] as $variable) {
EggVariable::query()->forceCreate(array_merge($variable, ['egg_id' => $egg->id]));
}

return $egg;
});
}
}
38 changes: 38 additions & 0 deletions resources/views/admin/nests/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<h3 class="box-title">Configured Nests</h3>
<div class="box-tools">
<a href="#" class="btn btn-sm btn-success" data-toggle="modal" data-target="#importServiceOptionModal" role="button"><i class="fa fa-upload"></i> Import Egg</a>
<a href="#" class="btn btn-sm btn-success" data-toggle="modal" data-target="#importServiceOptionFromUrlModal" role="button"><i class="fa fa-upload"></i> Import Egg from URL</a>
<a href="{{ route('admin.nests.new') }}" class="btn btn-primary btn-sm">Create New</a>
</div>
</div>
Expand Down Expand Up @@ -90,6 +91,43 @@
</div>
</div>
</div>
<div class="modal fade" tabindex="-1" role="dialog" id="importServiceOptionFromUrlModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Import an Egg</h4>
</div>
<form action="{{ route('admin.nests.egg.import_url') }}" enctype="multipart/form-data" method="POST">
<div class="modal-body">
<div class="form-group">
<label class="control-label" for="pImportFile">Egg URL <span class="field-required"></span></label>
<div>
<input id="pImportFile" type="url" name="import_file_url" class="form-control" accept="application/json" />
<p class="small text-muted">Type the URL of the file for the new egg that you wish to import.</p>
</div>
</div>
<div class="form-group">
<label class="control-label" for="pImportToNest">Associated Nest <span class="field-required"></span></label>
<div>
<select id="pImportToNest" name="import_to_nest">
@foreach($nests as $nest)
<option value="{{ $nest->id }}">{{ $nest->name }} &lt;{{ $nest->author }}&gt;</option>
@endforeach
</select>
<p class="small text-muted">Select the nest that this egg will be associated with from the dropdown. If you wish to associate it with a new nest you will need to create that nest before continuing.</p>
</div>
</div>
</div>
<div class="modal-footer">
{{ csrf_field() }}
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Import</button>
</div>
</form>
</div>
</div>
</div>
@endsection

@section('footer-scripts')
Expand Down
5 changes: 3 additions & 2 deletions routes/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@
Route::get('/', [Admin\Settings\DomainsController::class, 'index'])->name('admin.settings.domains.index');
Route::get('/create', [Admin\Settings\DomainsController::class, 'create'])->name('admin.settings.domains.create');
Route::get('/{domain}/edit', [Admin\Settings\DomainsController::class, 'edit'])->name('admin.settings.domains.edit');

Route::post('/', [Admin\Settings\DomainsController::class, 'store'])->name('admin.settings.domains.store');
Route::patch('/{domain}', [Admin\Settings\DomainsController::class, 'update'])->name('admin.settings.domains.update');
Route::delete('/{domain}', [Admin\Settings\DomainsController::class, 'destroy'])->name('admin.settings.domains.destroy');

Route::post('/test-connection', [Admin\Settings\DomainsController::class, 'testConnection'])->name('admin.settings.domains.test-connection');
Route::get('/provider-schema/{provider}', [Admin\Settings\DomainsController::class, 'getProviderSchema'])->name('admin.settings.domains.provider-schema');
});
Expand Down Expand Up @@ -227,6 +227,7 @@

Route::post('/new', [Admin\Nests\NestController::class, 'store']);
Route::post('/import', [Admin\Nests\EggShareController::class, 'import'])->name('admin.nests.egg.import');
Route::post('/importFromUrl', [Admin\Nests\EggShareController::class, 'importFromUrl'])->name('admin.nests.egg.import_url');
Route::post('/egg/new', [Admin\Nests\EggController::class, 'store']);
Route::post('/egg/{egg:id}/variables', [Admin\Nests\EggVariableController::class, 'store']);

Expand Down