]> BookStack Code Mirror - bookstack/commitdiff
ZIP Exports: Started import validation
authorDan Brown <redacted>
Wed, 30 Oct 2024 13:13:41 +0000 (13:13 +0000)
committerDan Brown <redacted>
Wed, 30 Oct 2024 13:13:41 +0000 (13:13 +0000)
app/Exceptions/ZipExportValidationException.php [new file with mode: 0644]
app/Exports/Controllers/ImportController.php
app/Exports/ZipExports/Models/ZipExportAttachment.php
app/Exports/ZipExports/Models/ZipExportModel.php
app/Exports/ZipExports/Models/ZipExportTag.php
app/Exports/ZipExports/ZipExportValidator.php [new file with mode: 0644]
app/Exports/ZipExports/ZipFileReferenceRule.php [new file with mode: 0644]
app/Exports/ZipExports/ZipValidationHelper.php [new file with mode: 0644]
lang/en/validation.php
resources/views/exports/import.blade.php

diff --git a/app/Exceptions/ZipExportValidationException.php b/app/Exceptions/ZipExportValidationException.php
new file mode 100644 (file)
index 0000000..2ed567d
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+namespace BookStack\Exceptions;
+
+class ZipExportValidationException extends \Exception
+{
+    public function __construct(
+        public array $errors,
+    ) {
+        parent::__construct();
+    }
+}
index 9eefb0974389bd4a6a3b9014f54ce74626ec8518..4270828effdcbe2fdf766bf744400de7ce645205 100644 (file)
@@ -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 - If invalid, return user with errors
         // TODO - Upload to storage
         // TODO - Read existing ZIP upload and send through validator
             // TODO - If invalid, return user with errors
         // TODO - Upload to storage
index 283ffa751c9a152679df1733e0cd75651efb3848..ab1f5ab7559129fde4c0b0e46f949e73509f2d1f 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Exports\ZipExports\Models;
 
 use BookStack\Exports\ZipExports\ZipExportFiles;
 namespace BookStack\Exports\ZipExports\Models;
 
 use BookStack\Exports\ZipExports\ZipExportFiles;
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 use BookStack\Uploads\Attachment;
 
 class ZipExportAttachment extends ZipExportModel
 use BookStack\Uploads\Attachment;
 
 class ZipExportAttachment extends ZipExportModel
@@ -35,4 +36,17 @@ class ZipExportAttachment extends ZipExportModel
             return self::fromModel($attachment, $files);
         }, $attachmentArray));
     }
             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);
+    }
 }
 }
index 8d0c0a4370b4f6adc8bbcf6c1c4c16319e8d2557..4d66f010f860f086b765faebcc0542e8d6796c26 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Exports\ZipExports\Models;
 
 
 namespace BookStack\Exports\ZipExports\Models;
 
+use BookStack\Exports\ZipExports\ZipValidationHelper;
 use JsonSerializable;
 
 abstract class ZipExportModel implements JsonSerializable
 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);
     }
         $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;
 }
 }
index d4e3c4290b33007150482aa6ce96b58c8ce33bd1..ad17d5a33c66b0b53d6928394d89fc8b1caac2cf 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Exports\ZipExports\Models;
 
 use BookStack\Activity\Models\Tag;
 namespace BookStack\Exports\ZipExports\Models;
 
 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);
+    }
 }
 }
diff --git a/app/Exports/ZipExports/ZipExportValidator.php b/app/Exports/ZipExports/ZipExportValidator.php
new file mode 100644 (file)
index 0000000..5ad9272
--- /dev/null
@@ -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);
+    }
+}
diff --git a/app/Exports/ZipExports/ZipFileReferenceRule.php b/app/Exports/ZipExports/ZipFileReferenceRule.php
new file mode 100644 (file)
index 0000000..4f942e0
--- /dev/null
@@ -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();
+        }
+    }
+}
diff --git a/app/Exports/ZipExports/ZipValidationHelper.php b/app/Exports/ZipExports/ZipValidationHelper.php
new file mode 100644 (file)
index 0000000..dd41e6f
--- /dev/null
@@ -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);
+    }
+}
index 2a676c7c4cce0130591a929cefd8a21e7765be1c..6971edc023a8771a428cbb7b475c394e5b1278f7 100644 (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.',
 
     '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' => [
     // Custom validation lines
     'custom' => [
         'password-confirm' => [
index b7030f114782491c82e0c97b109cb25af2e42da3..9fe596d8888f083f713dc0942f99be62fd587d6d 100644 (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">
                 {{ 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>
                         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>