namespace BookStack\Exports;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\FileUploadException;
use BookStack\Exceptions\ZipExportException;
use BookStack\Exceptions\ZipValidationException;
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;
{
public function __construct(
protected FileStorage $storage,
+ protected ZipImportRunner $importer,
+ protected EntityQueries $entityQueries,
) {
}
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();
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
--- /dev/null
+<?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;
+ }
+}
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;
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.
*/
*/
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.