ZIP Imports: Added parent and permission check pre-import
Some checks failed
analyse-php / build (push) Has been cancelled
lint-php / build (push) Has been cancelled
test-migrations / build (8.1) (push) Has been cancelled
test-migrations / build (8.2) (push) Has been cancelled
test-migrations / build (8.3) (push) Has been cancelled
test-php / build (8.1) (push) Has been cancelled
test-php / build (8.2) (push) Has been cancelled
test-php / build (8.3) (push) Has been cancelled

This commit is contained in:
Dan Brown 2024-11-05 15:41:58 +00:00
parent 92cfde495e
commit 7b84558ca1
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 195 additions and 12 deletions

View File

@ -0,0 +1,12 @@
<?php
namespace BookStack\Exceptions;
class ZipImportException extends \Exception
{
public function __construct(
public array $errors
) {
parent::__construct();
}
}

View File

@ -65,8 +65,6 @@ class ImportController extends Controller
{
$import = $this->imports->findVisible($id);
// dd($import->decodeMetadata());
$this->setPageTitle(trans('entities.import_continue'));
return view('exports.import-show', [

View File

@ -2,6 +2,7 @@
namespace BookStack\Exports;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\FileUploadException;
use BookStack\Exceptions\ZipExportException;
use BookStack\Exceptions\ZipValidationException;
@ -10,6 +11,7 @@ use BookStack\Exports\ZipExports\Models\ZipExportChapter;
use BookStack\Exports\ZipExports\Models\ZipExportPage;
use BookStack\Exports\ZipExports\ZipExportReader;
use BookStack\Exports\ZipExports\ZipExportValidator;
use BookStack\Exports\ZipExports\ZipImportRunner;
use BookStack\Uploads\FileStorage;
use Illuminate\Database\Eloquent\Collection;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -18,6 +20,8 @@ class ImportRepo
{
public function __construct(
protected FileStorage $storage,
protected ZipImportRunner $importer,
protected EntityQueries $entityQueries,
) {
}
@ -54,13 +58,13 @@ class ImportRepo
public function storeFromUpload(UploadedFile $file): Import
{
$zipPath = $file->getRealPath();
$reader = new ZipExportReader($zipPath);
$errors = (new ZipExportValidator($zipPath))->validate();
$errors = (new ZipExportValidator($reader))->validate();
if ($errors) {
throw new ZipValidationException($errors);
}
$reader = new ZipExportReader($zipPath);
$exportModel = $reader->decodeDataToExportModel();
$import = new Import();
@ -90,11 +94,17 @@ class ImportRepo
return $import;
}
/**
* @throws ZipValidationException
*/
public function runImport(Import $import, ?string $parent = null)
{
// TODO - Download import zip (if needed)
// TODO - Validate zip file again
// TODO - Check permissions before (create for main item, create for children, create for related items [image, attachments])
$parentModel = null;
if ($import->type === 'page' || $import->type === 'chapter') {
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
}
return $this->importer->run($import, $parentModel);
}
public function deleteImport(Import $import): void

View File

@ -10,20 +10,19 @@ use BookStack\Exports\ZipExports\Models\ZipExportPage;
class ZipExportValidator
{
public function __construct(
protected string $zipPath,
protected ZipExportReader $reader,
) {
}
public function validate(): array
{
$reader = new ZipExportReader($this->zipPath);
try {
$importData = $reader->readData();
$importData = $this->reader->readData();
} catch (ZipExportException $exception) {
return ['format' => $exception->getMessage()];
}
$helper = new ZipValidationHelper($reader);
$helper = new ZipValidationHelper($this->reader);
if (isset($importData['book'])) {
$modelErrors = ZipExportBook::validate($helper, $importData['book']);

View File

@ -0,0 +1,143 @@
<?php
namespace BookStack\Exports\ZipExports;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Exceptions\ZipExportException;
use BookStack\Exceptions\ZipImportException;
use BookStack\Exports\Import;
use BookStack\Exports\ZipExports\Models\ZipExportBook;
use BookStack\Exports\ZipExports\Models\ZipExportChapter;
use BookStack\Exports\ZipExports\Models\ZipExportPage;
use BookStack\Uploads\FileStorage;
class ZipImportRunner
{
public function __construct(
protected FileStorage $storage,
) {
}
/**
* @throws ZipImportException
*/
public function run(Import $import, ?Entity $parent = null): void
{
$zipPath = $this->getZipPath($import);
$reader = new ZipExportReader($zipPath);
$errors = (new ZipExportValidator($reader))->validate();
if ($errors) {
throw new ZipImportException(["ZIP failed to validate"]);
}
try {
$exportModel = $reader->decodeDataToExportModel();
} catch (ZipExportException $e) {
throw new ZipImportException([$e->getMessage()]);
}
// Validate parent type
if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
throw new ZipImportException(["Must not have a parent set for a Book import"]);
} else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
throw new ZipImportException(["Parent book required for chapter import"]);
} else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
throw new ZipImportException(["Parent book or chapter required for page import"]);
}
$this->ensurePermissionsPermitImport($exportModel);
// TODO - Run import
}
/**
* @throws ZipImportException
*/
protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
{
$errors = [];
// TODO - Extract messages to language files
// TODO - Ensure these are shown to users on failure
$chapters = [];
$pages = [];
$images = [];
$attachments = [];
if ($exportModel instanceof ZipExportBook) {
if (!userCan('book-create-all')) {
$errors[] = 'You are lacking the required permission to create books.';
}
array_push($pages, ...$exportModel->pages);
array_push($chapters, ...$exportModel->chapters);
} else if ($exportModel instanceof ZipExportChapter) {
$chapters[] = $exportModel;
} else if ($exportModel instanceof ZipExportPage) {
$pages[] = $exportModel;
}
foreach ($chapters as $chapter) {
array_push($pages, ...$chapter->pages);
}
if (count($chapters) > 0) {
$permission = 'chapter-create' . ($parent ? '' : '-all');
if (!userCan($permission, $parent)) {
$errors[] = 'You are lacking the required permission to create chapters.';
}
}
foreach ($pages as $page) {
array_push($attachments, ...$page->attachments);
array_push($images, ...$page->images);
}
if (count($pages) > 0) {
if ($parent) {
if (!userCan('page-create', $parent)) {
$errors[] = 'You are lacking the required permission to create pages.';
}
} else {
$hasPermission = userCan('page-create-all') || userCan('page-create-own');
if (!$hasPermission) {
$errors[] = 'You are lacking the required permission to create pages.';
}
}
}
if (count($images) > 0) {
if (!userCan('image-create-all')) {
$errors[] = 'You are lacking the required permissions to create images.';
}
}
if (count($attachments) > 0) {
if (userCan('attachment-create-all')) {
$errors[] = 'You are lacking the required permissions to create attachments.';
}
}
if (count($errors)) {
throw new ZipImportException($errors);
}
}
protected function getZipPath(Import $import): string
{
if (!$this->storage->isRemote()) {
return $this->storage->getSystemPath($import->path);
}
$tempFilePath = tempnam(sys_get_temp_dir(), 'bszip-import-');
$tempFile = fopen($tempFilePath, 'wb');
$stream = $this->storage->getReadStream($import->path);
stream_copy_to_stream($stream, $tempFile);
fclose($tempFile);
return $tempFilePath;
}
}

View File

@ -5,6 +5,7 @@ namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException;
use Exception;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
@ -70,6 +71,26 @@ class FileStorage
return $filePath;
}
/**
* Check whether the configured storage is remote from the host of this app.
*/
public function isRemote(): bool
{
return $this->getStorageDiskName() === 's3';
}
/**
* Get the actual path on system for the given relative file path.
*/
public function getSystemPath(string $filePath): string
{
if ($this->isRemote()) {
return '';
}
return storage_path('uploads/files/' . ltrim($this->adjustPathForStorageDisk($filePath), '/'));
}
/**
* Get the storage that will be used for storing files.
*/
@ -83,7 +104,7 @@ class FileStorage
*/
protected function getStorageDiskName(): string
{
$storageType = config('filesystems.attachments');
$storageType = trim(strtolower(config('filesystems.attachments')));
// Change to our secure-attachment disk if any of the local options
// are used to prevent escaping that location.