mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-22 06:38:26 +08:00
ZIP Exports: Got zip format validation functionally complete
Some checks failed
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.1) (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-php / build (8.1) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
Some checks failed
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.1) (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-php / build (8.1) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled
This commit is contained in:
parent
b50b7b667d
commit
c4ec50d437
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
class ZipExportValidationException extends \Exception
|
||||
{
|
||||
public function __construct(
|
||||
public array $errors,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Exports\Controllers;
|
||||
|
||||
use BookStack\Exports\ZipExports\ZipExportValidator;
|
||||
use BookStack\Http\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
@ -26,7 +27,13 @@ class ImportController extends Controller
|
|||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$file->getRealPath();
|
||||
$zipPath = $file->getRealPath();
|
||||
|
||||
$errors = (new ZipExportValidator($zipPath))->validate();
|
||||
if ($errors) {
|
||||
dd($errors);
|
||||
}
|
||||
dd('passed');
|
||||
// TODO - Read existing ZIP upload and send through validator
|
||||
// TODO - If invalid, return user with errors
|
||||
// TODO - Upload to storage
|
||||
|
|
|
@ -47,6 +47,6 @@ class ZipExportAttachment extends ZipExportModel
|
|||
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
|
||||
];
|
||||
|
||||
return $context->validateArray($data, $rules);
|
||||
return $context->validateData($data, $rules);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use BookStack\Entities\Models\Book;
|
|||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exports\ZipExports\ZipExportFiles;
|
||||
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||
|
||||
class ZipExportBook extends ZipExportModel
|
||||
{
|
||||
|
@ -50,4 +51,24 @@ class ZipExportBook extends ZipExportModel
|
|||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public static function validate(ZipValidationHelper $context, array $data): array
|
||||
{
|
||||
$rules = [
|
||||
'id' => ['nullable', 'int'],
|
||||
'name' => ['required', 'string', 'min:1'],
|
||||
'description_html' => ['nullable', 'string'],
|
||||
'cover' => ['nullable', 'string', $context->fileReferenceRule()],
|
||||
'tags' => ['array'],
|
||||
'pages' => ['array'],
|
||||
'chapters' => ['array'],
|
||||
];
|
||||
|
||||
$errors = $context->validateData($data, $rules);
|
||||
$errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
|
||||
$errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);
|
||||
$errors['chapters'] = $context->validateRelations($data['chapters'] ?? [], ZipExportChapter::class);
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace BookStack\Exports\ZipExports\Models;
|
|||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exports\ZipExports\ZipExportFiles;
|
||||
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||
|
||||
class ZipExportChapter extends ZipExportModel
|
||||
{
|
||||
|
@ -42,4 +43,22 @@ class ZipExportChapter extends ZipExportModel
|
|||
return self::fromModel($chapter, $files);
|
||||
}, $chapterArray));
|
||||
}
|
||||
|
||||
public static function validate(ZipValidationHelper $context, array $data): array
|
||||
{
|
||||
$rules = [
|
||||
'id' => ['nullable', 'int'],
|
||||
'name' => ['required', 'string', 'min:1'],
|
||||
'description_html' => ['nullable', 'string'],
|
||||
'priority' => ['nullable', 'int'],
|
||||
'tags' => ['array'],
|
||||
'pages' => ['array'],
|
||||
];
|
||||
|
||||
$errors = $context->validateData($data, $rules);
|
||||
$errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
|
||||
$errors['pages'] = $context->validateRelations($data['pages'] ?? [], ZipExportPage::class);
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
namespace BookStack\Exports\ZipExports\Models;
|
||||
|
||||
use BookStack\Exports\ZipExports\ZipExportFiles;
|
||||
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||
use BookStack\Uploads\Image;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ZipExportImage extends ZipExportModel
|
||||
{
|
||||
|
@ -22,4 +24,16 @@ class ZipExportImage extends ZipExportModel
|
|||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public static function validate(ZipValidationHelper $context, array $data): array
|
||||
{
|
||||
$rules = [
|
||||
'id' => ['nullable', 'int'],
|
||||
'name' => ['required', 'string', 'min:1'],
|
||||
'file' => ['required', 'string', $context->fileReferenceRule()],
|
||||
'type' => ['required', 'string', Rule::in(['gallery', 'drawio'])],
|
||||
];
|
||||
|
||||
return $context->validateData($data, $rules);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace BookStack\Exports\ZipExports\Models;
|
|||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Exports\ZipExports\ZipExportFiles;
|
||||
use BookStack\Exports\ZipExports\ZipValidationHelper;
|
||||
|
||||
class ZipExportPage extends ZipExportModel
|
||||
{
|
||||
|
@ -48,4 +49,25 @@ class ZipExportPage extends ZipExportModel
|
|||
return self::fromModel($page, $files);
|
||||
}, $pageArray));
|
||||
}
|
||||
|
||||
public static function validate(ZipValidationHelper $context, array $data): array
|
||||
{
|
||||
$rules = [
|
||||
'id' => ['nullable', 'int'],
|
||||
'name' => ['required', 'string', 'min:1'],
|
||||
'html' => ['nullable', 'string'],
|
||||
'markdown' => ['nullable', 'string'],
|
||||
'priority' => ['nullable', 'int'],
|
||||
'attachments' => ['array'],
|
||||
'images' => ['array'],
|
||||
'tags' => ['array'],
|
||||
];
|
||||
|
||||
$errors = $context->validateData($data, $rules);
|
||||
$errors['attachments'] = $context->validateRelations($data['attachments'] ?? [], ZipExportAttachment::class);
|
||||
$errors['images'] = $context->validateRelations($data['images'] ?? [], ZipExportImage::class);
|
||||
$errors['tags'] = $context->validateRelations($data['tags'] ?? [], ZipExportTag::class);
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,6 @@ class ZipExportTag extends ZipExportModel
|
|||
'order' => ['nullable', 'integer'],
|
||||
];
|
||||
|
||||
return $context->validateArray($data, $rules);
|
||||
return $context->validateData($data, $rules);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,62 +2,69 @@
|
|||
|
||||
namespace BookStack\Exports\ZipExports;
|
||||
|
||||
use BookStack\Exceptions\ZipExportValidationException;
|
||||
use BookStack\Exports\ZipExports\Models\ZipExportBook;
|
||||
use BookStack\Exports\ZipExports\Models\ZipExportChapter;
|
||||
use BookStack\Exports\ZipExports\Models\ZipExportPage;
|
||||
use ZipArchive;
|
||||
|
||||
class ZipExportValidator
|
||||
{
|
||||
protected array $errors = [];
|
||||
|
||||
public function __construct(
|
||||
protected string $zipPath,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZipExportValidationException
|
||||
*/
|
||||
public function validate()
|
||||
public function validate(): array
|
||||
{
|
||||
// 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");
|
||||
return ['format' => "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");
|
||||
return ['format' => "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");
|
||||
return ['format' => "Could not find and decode ZIP data.json content"];
|
||||
}
|
||||
|
||||
$helper = new ZipValidationHelper($zip);
|
||||
|
||||
if (isset($importData['book'])) {
|
||||
// TODO - Validate book
|
||||
$modelErrors = ZipExportBook::validate($helper, $importData['book']);
|
||||
$keyPrefix = 'book';
|
||||
} else if (isset($importData['chapter'])) {
|
||||
// TODO - Validate chapter
|
||||
$modelErrors = ZipExportChapter::validate($helper, $importData['chapter']);
|
||||
$keyPrefix = 'chapter';
|
||||
} else if (isset($importData['page'])) {
|
||||
// TODO - Validate page
|
||||
$modelErrors = ZipExportPage::validate($helper, $importData['page']);
|
||||
$keyPrefix = 'page';
|
||||
} else {
|
||||
$this->throwErrors("ZIP file has no book, chapter or page data");
|
||||
return ['format' => "ZIP file has no book, chapter or page data"];
|
||||
}
|
||||
|
||||
return $this->flattenModelErrors($modelErrors, $keyPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZipExportValidationException
|
||||
*/
|
||||
protected function throwErrors(...$errorsToAdd): never
|
||||
protected function flattenModelErrors(array $errors, string $keyPrefix): array
|
||||
{
|
||||
array_push($this->errors, ...$errorsToAdd);
|
||||
throw new ZipExportValidationException($this->errors);
|
||||
$flattened = [];
|
||||
|
||||
foreach ($errors as $key => $error) {
|
||||
if (is_array($error)) {
|
||||
$flattened = array_merge($flattened, $this->flattenModelErrors($error, $keyPrefix . '.' . $key));
|
||||
} else {
|
||||
$flattened[$keyPrefix . '.' . $key] = $error;
|
||||
}
|
||||
}
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Exports\ZipExports;
|
||||
|
||||
use BookStack\Exports\ZipExports\Models\ZipExportModel;
|
||||
use Illuminate\Validation\Factory;
|
||||
use ZipArchive;
|
||||
|
||||
|
@ -15,9 +16,15 @@ class ZipValidationHelper
|
|||
$this->validationFactory = app(Factory::class);
|
||||
}
|
||||
|
||||
public function validateArray(array $data, array $rules): array
|
||||
public function validateData(array $data, array $rules): array
|
||||
{
|
||||
return $this->validationFactory->make($data, $rules)->errors()->messages();
|
||||
$messages = $this->validationFactory->make($data, $rules)->errors()->messages();
|
||||
|
||||
foreach ($messages as $key => $message) {
|
||||
$messages[$key] = implode("\n", $message);
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
public function zipFileExists(string $name): bool
|
||||
|
@ -29,4 +36,24 @@ class ZipValidationHelper
|
|||
{
|
||||
return new ZipFileReferenceRule($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an array of relation data arrays that are expected
|
||||
* to be for the given ZipExportModel.
|
||||
* @param class-string<ZipExportModel> $model
|
||||
*/
|
||||
public function validateRelations(array $relations, string $model): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($relations as $key => $relationData) {
|
||||
if (is_array($relationData)) {
|
||||
$results[$key] = $model::validate($this, $relationData);
|
||||
} else {
|
||||
$results[$key] = [trans('validation.zip_model_expected', ['type' => gettype($relationData)])];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +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.',
|
||||
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
|
||||
'zip_model_expected' => 'Data object expected but ":type" found',
|
||||
|
||||
// Custom validation lines
|
||||
'custom' => [
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<main class="card content-wrap auto-height mt-xxl">
|
||||
<h1 class="list-heading">{{ trans('entities.import') }}</h1>
|
||||
<form action="{{ url('/import') }}" method="POST">
|
||||
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
|
||||
{{ 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">
|
||||
|
@ -22,6 +22,7 @@
|
|||
name="file"
|
||||
id="file"
|
||||
class="custom-simple-file-input">
|
||||
@include('form.errors', ['name' => 'file'])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user