--- /dev/null
+<?php
+
+namespace BookStack\Exceptions;
+
+class ZipExportValidationException extends \Exception
+{
+ public function __construct(
+ public array $errors,
+ ) {
+ parent::__construct();
+ }
+}
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
namespace BookStack\Exports\ZipExports\Models;
use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
use BookStack\Uploads\Attachment;
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);
+ }
}
namespace BookStack\Exports\ZipExports\Models;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
use JsonSerializable;
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;
}
namespace BookStack\Exports\ZipExports\Models;
use BookStack\Activity\Models\Tag;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
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);
+ }
}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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();
+ }
+ }
+}
--- /dev/null
+<?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);
+ }
+}
'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' => [
{{ 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>