Skip to content

Commit b50b7b6

Browse files
committed
ZIP Exports: Started import validation
1 parent a56a28f commit b50b7b6

File tree

10 files changed

+177
-1
lines changed

10 files changed

+177
-1
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace BookStack\Exceptions;
4+
5+
class ZipExportValidationException extends \Exception
6+
{
7+
public function __construct(
8+
public array $errors,
9+
) {
10+
parent::__construct();
11+
}
12+
}

app/Exports/Controllers/ImportController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public function start(Request $request)
2121

2222
public function upload(Request $request)
2323
{
24+
$this->validate($request, [
25+
'file' => ['required', 'file']
26+
]);
27+
28+
$file = $request->file('file');
29+
$file->getRealPath();
2430
// TODO - Read existing ZIP upload and send through validator
2531
// TODO - If invalid, return user with errors
2632
// TODO - Upload to storage

app/Exports/ZipExports/Models/ZipExportAttachment.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BookStack\Exports\ZipExports\Models;
44

55
use BookStack\Exports\ZipExports\ZipExportFiles;
6+
use BookStack\Exports\ZipExports\ZipValidationHelper;
67
use BookStack\Uploads\Attachment;
78

89
class ZipExportAttachment extends ZipExportModel
@@ -35,4 +36,17 @@ public static function fromModelArray(array $attachmentArray, ZipExportFiles $fi
3536
return self::fromModel($attachment, $files);
3637
}, $attachmentArray));
3738
}
39+
40+
public static function validate(ZipValidationHelper $context, array $data): array
41+
{
42+
$rules = [
43+
'id' => ['nullable', 'int'],
44+
'name' => ['required', 'string', 'min:1'],
45+
'order' => ['nullable', 'integer'],
46+
'link' => ['required_without:file', 'nullable', 'string'],
47+
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
48+
];
49+
50+
return $context->validateArray($data, $rules);
51+
}
3852
}

app/Exports/ZipExports/Models/ZipExportModel.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace BookStack\Exports\ZipExports\Models;
44

5+
use BookStack\Exports\ZipExports\ZipValidationHelper;
56
use JsonSerializable;
67

78
abstract class ZipExportModel implements JsonSerializable
@@ -17,4 +18,12 @@ public function jsonSerialize(): array
1718
$publicProps = get_object_vars(...)->__invoke($this);
1819
return array_filter($publicProps, fn ($value) => $value !== null);
1920
}
21+
22+
/**
23+
* Validate the given array of data intended for this model.
24+
* Return an array of validation errors messages.
25+
* Child items can be considered in the validation result by returning a keyed
26+
* item in the array for its own validation messages.
27+
*/
28+
abstract public static function validate(ZipValidationHelper $context, array $data): array;
2029
}

app/Exports/ZipExports/Models/ZipExportTag.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BookStack\Exports\ZipExports\Models;
44

55
use BookStack\Activity\Models\Tag;
6+
use BookStack\Exports\ZipExports\ZipValidationHelper;
67

78
class ZipExportTag extends ZipExportModel
89
{
@@ -24,4 +25,15 @@ public static function fromModelArray(array $tagArray): array
2425
{
2526
return array_values(array_map(self::fromModel(...), $tagArray));
2627
}
28+
29+
public static function validate(ZipValidationHelper $context, array $data): array
30+
{
31+
$rules = [
32+
'name' => ['required', 'string', 'min:1'],
33+
'value' => ['nullable', 'string'],
34+
'order' => ['nullable', 'integer'],
35+
];
36+
37+
return $context->validateArray($data, $rules);
38+
}
2739
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace BookStack\Exports\ZipExports;
4+
5+
use BookStack\Exceptions\ZipExportValidationException;
6+
use ZipArchive;
7+
8+
class ZipExportValidator
9+
{
10+
protected array $errors = [];
11+
12+
public function __construct(
13+
protected string $zipPath,
14+
) {
15+
}
16+
17+
/**
18+
* @throws ZipExportValidationException
19+
*/
20+
public function validate()
21+
{
22+
// TODO - Return type
23+
// TODO - extract messages to translations?
24+
25+
// Validate file exists
26+
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
27+
$this->throwErrors("Could not read ZIP file");
28+
}
29+
30+
// Validate file is valid zip
31+
$zip = new \ZipArchive();
32+
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
33+
if ($opened !== true) {
34+
$this->throwErrors("Could not read ZIP file");
35+
}
36+
37+
// Validate json data exists, including metadata
38+
$jsonData = $zip->getFromName('data.json') ?: '';
39+
$importData = json_decode($jsonData, true);
40+
if (!$importData) {
41+
$this->throwErrors("Could not decode ZIP data.json content");
42+
}
43+
44+
if (isset($importData['book'])) {
45+
// TODO - Validate book
46+
} else if (isset($importData['chapter'])) {
47+
// TODO - Validate chapter
48+
} else if (isset($importData['page'])) {
49+
// TODO - Validate page
50+
} else {
51+
$this->throwErrors("ZIP file has no book, chapter or page data");
52+
}
53+
}
54+
55+
/**
56+
* @throws ZipExportValidationException
57+
*/
58+
protected function throwErrors(...$errorsToAdd): never
59+
{
60+
array_push($this->errors, ...$errorsToAdd);
61+
throw new ZipExportValidationException($this->errors);
62+
}
63+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace BookStack\Exports\ZipExports;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
use ZipArchive;
8+
9+
class ZipFileReferenceRule implements ValidationRule
10+
{
11+
public function __construct(
12+
protected ZipValidationHelper $context,
13+
) {
14+
}
15+
16+
17+
/**
18+
* @inheritDoc
19+
*/
20+
public function validate(string $attribute, mixed $value, Closure $fail): void
21+
{
22+
if (!$this->context->zipFileExists($value)) {
23+
$fail('validation.zip_file')->translate();
24+
}
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace BookStack\Exports\ZipExports;
4+
5+
use Illuminate\Validation\Factory;
6+
use ZipArchive;
7+
8+
class ZipValidationHelper
9+
{
10+
protected Factory $validationFactory;
11+
12+
public function __construct(
13+
protected ZipArchive $zip,
14+
) {
15+
$this->validationFactory = app(Factory::class);
16+
}
17+
18+
public function validateArray(array $data, array $rules): array
19+
{
20+
return $this->validationFactory->make($data, $rules)->errors()->messages();
21+
}
22+
23+
public function zipFileExists(string $name): bool
24+
{
25+
return $this->zip->statName("files/{$name}") !== false;
26+
}
27+
28+
public function fileReferenceRule(): ZipFileReferenceRule
29+
{
30+
return new ZipFileReferenceRule($this);
31+
}
32+
}

lang/en/validation.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
'url' => 'The :attribute format is invalid.',
106106
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
107107

108+
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
109+
108110
// Custom validation lines
109111
'custom' => [
110112
'password-confirm' => [

resources/views/exports/import.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{{ csrf_field() }}
1111
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
1212
<p class="flex min-width-l text-muted mb-s">
13-
Import content using a portable zip export from the same, or a different, instance.
13+
Import books, chapters & pages using a portable zip export from the same, or a different, instance.
1414
Select a ZIP file to import then press "Validate Import" to proceed.
1515
After the file has been uploaded and validated you'll be able to configure & confirm the import in the next view.
1616
</p>

0 commit comments

Comments
 (0)