ZIP Exports: Started import validation

This commit is contained in:
Dan Brown 2024-10-30 13:13:41 +00:00
parent a56a28fbb7
commit b50b7b667d
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 177 additions and 1 deletions

View File

@ -0,0 +1,12 @@
<?php
namespace BookStack\Exceptions;
class ZipExportValidationException extends \Exception
{
public function __construct(
public array $errors,
) {
parent::__construct();
}
}

View File

@ -21,6 +21,12 @@ class ImportController extends Controller
public function upload(Request $request)
{
$this->validate($request, [
'file' => ['required', 'file']
]);
$file = $request->file('file');
$file->getRealPath();
// TODO - Read existing ZIP upload and send through validator
// TODO - If invalid, return user with errors
// TODO - Upload to storage

View File

@ -3,6 +3,7 @@
namespace BookStack\Exports\ZipExports\Models;
use BookStack\Exports\ZipExports\ZipExportFiles;
use BookStack\Exports\ZipExports\ZipValidationHelper;
use BookStack\Uploads\Attachment;
class ZipExportAttachment extends ZipExportModel
@ -35,4 +36,17 @@ class ZipExportAttachment extends ZipExportModel
return self::fromModel($attachment, $files);
}, $attachmentArray));
}
public static function validate(ZipValidationHelper $context, array $data): array
{
$rules = [
'id' => ['nullable', 'int'],
'name' => ['required', 'string', 'min:1'],
'order' => ['nullable', 'integer'],
'link' => ['required_without:file', 'nullable', 'string'],
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
];
return $context->validateArray($data, $rules);
}
}

View File

@ -2,6 +2,7 @@
namespace BookStack\Exports\ZipExports\Models;
use BookStack\Exports\ZipExports\ZipValidationHelper;
use JsonSerializable;
abstract class ZipExportModel implements JsonSerializable
@ -17,4 +18,12 @@ abstract class ZipExportModel implements JsonSerializable
$publicProps = get_object_vars(...)->__invoke($this);
return array_filter($publicProps, fn ($value) => $value !== null);
}
/**
* Validate the given array of data intended for this model.
* Return an array of validation errors messages.
* Child items can be considered in the validation result by returning a keyed
* item in the array for its own validation messages.
*/
abstract public static function validate(ZipValidationHelper $context, array $data): array;
}

View File

@ -3,6 +3,7 @@
namespace BookStack\Exports\ZipExports\Models;
use BookStack\Activity\Models\Tag;
use BookStack\Exports\ZipExports\ZipValidationHelper;
class ZipExportTag extends ZipExportModel
{
@ -24,4 +25,15 @@ class ZipExportTag extends ZipExportModel
{
return array_values(array_map(self::fromModel(...), $tagArray));
}
public static function validate(ZipValidationHelper $context, array $data): array
{
$rules = [
'name' => ['required', 'string', 'min:1'],
'value' => ['nullable', 'string'],
'order' => ['nullable', 'integer'],
];
return $context->validateArray($data, $rules);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace BookStack\Exports\ZipExports;
use BookStack\Exceptions\ZipExportValidationException;
use ZipArchive;
class ZipExportValidator
{
protected array $errors = [];
public function __construct(
protected string $zipPath,
) {
}
/**
* @throws ZipExportValidationException
*/
public function validate()
{
// TODO - Return type
// TODO - extract messages to translations?
// Validate file exists
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
$this->throwErrors("Could not read ZIP file");
}
// Validate file is valid zip
$zip = new \ZipArchive();
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
if ($opened !== true) {
$this->throwErrors("Could not read ZIP file");
}
// Validate json data exists, including metadata
$jsonData = $zip->getFromName('data.json') ?: '';
$importData = json_decode($jsonData, true);
if (!$importData) {
$this->throwErrors("Could not decode ZIP data.json content");
}
if (isset($importData['book'])) {
// TODO - Validate book
} else if (isset($importData['chapter'])) {
// TODO - Validate chapter
} else if (isset($importData['page'])) {
// TODO - Validate page
} else {
$this->throwErrors("ZIP file has no book, chapter or page data");
}
}
/**
* @throws ZipExportValidationException
*/
protected function throwErrors(...$errorsToAdd): never
{
array_push($this->errors, ...$errorsToAdd);
throw new ZipExportValidationException($this->errors);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace BookStack\Exports\ZipExports;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use ZipArchive;
class ZipFileReferenceRule implements ValidationRule
{
public function __construct(
protected ZipValidationHelper $context,
) {
}
/**
* @inheritDoc
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!$this->context->zipFileExists($value)) {
$fail('validation.zip_file')->translate();
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace BookStack\Exports\ZipExports;
use Illuminate\Validation\Factory;
use ZipArchive;
class ZipValidationHelper
{
protected Factory $validationFactory;
public function __construct(
protected ZipArchive $zip,
) {
$this->validationFactory = app(Factory::class);
}
public function validateArray(array $data, array $rules): array
{
return $this->validationFactory->make($data, $rules)->errors()->messages();
}
public function zipFileExists(string $name): bool
{
return $this->zip->statName("files/{$name}") !== false;
}
public function fileReferenceRule(): ZipFileReferenceRule
{
return new ZipFileReferenceRule($this);
}
}

View File

@ -105,6 +105,8 @@ return [
'url' => 'The :attribute format is invalid.',
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
// Custom validation lines
'custom' => [
'password-confirm' => [

View File

@ -10,7 +10,7 @@
{{ csrf_field() }}
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
<p class="flex min-width-l text-muted mb-s">
Import content using a portable zip export from the same, or a different, instance.
Import books, chapters & pages using a portable zip export from the same, or a different, instance.
Select a ZIP file to import then press "Validate Import" to proceed.
After the file has been uploaded and validated you'll be able to configure & confirm the import in the next view.
</p>