Skip to content

Commit

Permalink
ZIP Exports: Got zip format validation functionally complete
Browse files Browse the repository at this point in the history
  • Loading branch information
ssddanbrown committed Oct 30, 2024
1 parent b50b7b6 commit c4ec50d
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 42 deletions.
12 changes: 0 additions & 12 deletions app/Exceptions/ZipExportValidationException.php

This file was deleted.

9 changes: 8 additions & 1 deletion app/Exports/Controllers/ImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace BookStack\Exports\Controllers;

use BookStack\Exports\ZipExports\ZipExportValidator;
use BookStack\Http\Controller;
use Illuminate\Http\Request;

Expand All @@ -26,7 +27,13 @@ public function upload(Request $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
Expand Down
2 changes: 1 addition & 1 deletion app/Exports/ZipExports/Models/ZipExportAttachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ public static function validate(ZipValidationHelper $context, array $data): arra
'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()],
];

return $context->validateArray($data, $rules);
return $context->validateData($data, $rules);
}
}
21 changes: 21 additions & 0 deletions app/Exports/ZipExports/Models/ZipExportBook.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExports\ZipExportFiles;
use BookStack\Exports\ZipExports\ZipValidationHelper;

class ZipExportBook extends ZipExportModel
{
Expand Down Expand Up @@ -50,4 +51,24 @@ public static function fromModel(Book $model, ZipExportFiles $files): self

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;
}
}
19 changes: 19 additions & 0 deletions app/Exports/ZipExports/Models/ZipExportChapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exports\ZipExports\ZipExportFiles;
use BookStack\Exports\ZipExports\ZipValidationHelper;

class ZipExportChapter extends ZipExportModel
{
Expand Down Expand Up @@ -42,4 +43,22 @@ public static function fromModelArray(array $chapterArray, ZipExportFiles $files
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;
}
}
14 changes: 14 additions & 0 deletions app/Exports/ZipExports/Models/ZipExportImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -22,4 +24,16 @@ public static function fromModel(Image $model, ZipExportFiles $files): self

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);
}
}
22 changes: 22 additions & 0 deletions app/Exports/ZipExports/Models/ZipExportPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exports\ZipExports\ZipExportFiles;
use BookStack\Exports\ZipExports\ZipValidationHelper;

class ZipExportPage extends ZipExportModel
{
Expand Down Expand Up @@ -48,4 +49,25 @@ public static function fromModelArray(array $pageArray, ZipExportFiles $files):
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;
}
}
2 changes: 1 addition & 1 deletion app/Exports/ZipExports/Models/ZipExportTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ public static function validate(ZipValidationHelper $context, array $data): arra
'order' => ['nullable', 'integer'],
];

return $context->validateArray($data, $rules);
return $context->validateData($data, $rules);
}
}
53 changes: 30 additions & 23 deletions app/Exports/ZipExports/ZipExportValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
31 changes: 29 additions & 2 deletions app/Exports/ZipExports/ZipValidationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace BookStack\Exports\ZipExports;

use BookStack\Exports\ZipExports\Models\ZipExportModel;
use Illuminate\Validation\Factory;
use ZipArchive;

Expand All @@ -15,9 +16,15 @@ public function __construct(
$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
Expand All @@ -29,4 +36,24 @@ public function fileReferenceRule(): ZipFileReferenceRule
{
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;
}
}
3 changes: 2 additions & 1 deletion lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
'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' => [
Expand Down
3 changes: 2 additions & 1 deletion resources/views/exports/import.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -22,6 +22,7 @@
name="file"
id="file"
class="custom-simple-file-input">
@include('form.errors', ['name' => 'file'])
</div>
</div>
</div>
Expand Down

0 comments on commit c4ec50d

Please sign in to comment.