ZIP Exports: Started import validation
This commit is contained in:
parent
a56a28fbb7
commit
b50b7b667d
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
class ZipExportValidationException extends \Exception
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public array $errors,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,12 @@ class ImportController extends Controller
|
||||||
|
|
||||||
public function upload(Request $request)
|
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 - Read existing ZIP upload and send through validator
|
||||||
// TODO - If invalid, return user with errors
|
// TODO - If invalid, return user with errors
|
||||||
// TODO - Upload to storage
|
// TODO - Upload to storage
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BookStack\Exports\ZipExports\Models;
|
namespace BookStack\Exports\ZipExports\Models;
|
||||||
|
|
||||||
use BookStack\Exports\ZipExports\ZipExportFiles;
|
use BookStack\Exports\ZipExports\ZipExportFiles;
|
||||||
|
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||||
use BookStack\Uploads\Attachment;
|
use BookStack\Uploads\Attachment;
|
||||||
|
|
||||||
class ZipExportAttachment extends ZipExportModel
|
class ZipExportAttachment extends ZipExportModel
|
||||||
|
@ -35,4 +36,17 @@ class ZipExportAttachment extends ZipExportModel
|
||||||
return self::fromModel($attachment, $files);
|
return self::fromModel($attachment, $files);
|
||||||
}, $attachmentArray));
|
}, $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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace BookStack\Exports\ZipExports\Models;
|
namespace BookStack\Exports\ZipExports\Models;
|
||||||
|
|
||||||
|
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
abstract class ZipExportModel implements JsonSerializable
|
abstract class ZipExportModel implements JsonSerializable
|
||||||
|
@ -17,4 +18,12 @@ abstract class ZipExportModel implements JsonSerializable
|
||||||
$publicProps = get_object_vars(...)->__invoke($this);
|
$publicProps = get_object_vars(...)->__invoke($this);
|
||||||
return array_filter($publicProps, fn ($value) => $value !== null);
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BookStack\Exports\ZipExports\Models;
|
namespace BookStack\Exports\ZipExports\Models;
|
||||||
|
|
||||||
use BookStack\Activity\Models\Tag;
|
use BookStack\Activity\Models\Tag;
|
||||||
|
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||||
|
|
||||||
class ZipExportTag extends ZipExportModel
|
class ZipExportTag extends ZipExportModel
|
||||||
{
|
{
|
||||||
|
@ -24,4 +25,15 @@ class ZipExportTag extends ZipExportModel
|
||||||
{
|
{
|
||||||
return array_values(array_map(self::fromModel(...), $tagArray));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,6 +105,8 @@ return [
|
||||||
'url' => 'The :attribute format is invalid.',
|
'url' => 'The :attribute format is invalid.',
|
||||||
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
|
'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 validation lines
|
||||||
'custom' => [
|
'custom' => [
|
||||||
'password-confirm' => [
|
'password-confirm' => [
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
|
<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">
|
<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.
|
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.
|
After the file has been uploaded and validated you'll be able to configure & confirm the import in the next view.
|
||||||
</p>
|
</p>
|
||||||
|
|
Loading…
Reference in New Issue