+++ /dev/null
-<?php
-
-namespace BookStack\Exceptions;
-
-class ZipExportValidationException extends \Exception
-{
- public function __construct(
- public array $errors,
- ) {
- parent::__construct();
- }
-}
namespace BookStack\Exports\Controllers;
+use BookStack\Exports\ZipExports\ZipExportValidator;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
]);
$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
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
];
- return $context->validateArray($data, $rules);
+ return $context->validateData($data, $rules);
}
}
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
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;
+ }
}
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
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;
+ }
}
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
{
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);
+ }
}
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
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;
+ }
}
'order' => ['nullable', 'integer'],
];
- return $context->validateArray($data, $rules);
+ return $context->validateData($data, $rules);
}
}
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;
}
}
namespace BookStack\Exports\ZipExports;
+use BookStack\Exports\ZipExports\Models\ZipExportModel;
use Illuminate\Validation\Factory;
use ZipArchive;
$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
{
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;
+ }
}
'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' => [
<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">
name="file"
id="file"
class="custom-simple-file-input">
+ @include('form.errors', ['name' => 'file'])
</div>
</div>
</div>