Skip to content

Commit 7b84558

Browse files
committed
ZIP Imports: Added parent and permission check pre-import
1 parent 92cfde4 commit 7b84558

File tree

6 files changed

+195
-12
lines changed

6 files changed

+195
-12
lines changed

app/Exceptions/ZipImportException.php

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 ZipImportException extends \Exception
6+
{
7+
public function __construct(
8+
public array $errors
9+
) {
10+
parent::__construct();
11+
}
12+
}

app/Exports/Controllers/ImportController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ public function show(int $id)
6565
{
6666
$import = $this->imports->findVisible($id);
6767

68-
// dd($import->decodeMetadata());
69-
7068
$this->setPageTitle(trans('entities.import_continue'));
7169

7270
return view('exports.import-show', [

app/Exports/ImportRepo.php

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

33
namespace BookStack\Exports;
44

5+
use BookStack\Entities\Queries\EntityQueries;
56
use BookStack\Exceptions\FileUploadException;
67
use BookStack\Exceptions\ZipExportException;
78
use BookStack\Exceptions\ZipValidationException;
@@ -10,6 +11,7 @@
1011
use BookStack\Exports\ZipExports\Models\ZipExportPage;
1112
use BookStack\Exports\ZipExports\ZipExportReader;
1213
use BookStack\Exports\ZipExports\ZipExportValidator;
14+
use BookStack\Exports\ZipExports\ZipImportRunner;
1315
use BookStack\Uploads\FileStorage;
1416
use Illuminate\Database\Eloquent\Collection;
1517
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -18,6 +20,8 @@ class ImportRepo
1820
{
1921
public function __construct(
2022
protected FileStorage $storage,
23+
protected ZipImportRunner $importer,
24+
protected EntityQueries $entityQueries,
2125
) {
2226
}
2327

@@ -54,13 +58,13 @@ public function findVisible(int $id): Import
5458
public function storeFromUpload(UploadedFile $file): Import
5559
{
5660
$zipPath = $file->getRealPath();
61+
$reader = new ZipExportReader($zipPath);
5762

58-
$errors = (new ZipExportValidator($zipPath))->validate();
63+
$errors = (new ZipExportValidator($reader))->validate();
5964
if ($errors) {
6065
throw new ZipValidationException($errors);
6166
}
6267

63-
$reader = new ZipExportReader($zipPath);
6468
$exportModel = $reader->decodeDataToExportModel();
6569

6670
$import = new Import();
@@ -90,11 +94,17 @@ public function storeFromUpload(UploadedFile $file): Import
9094
return $import;
9195
}
9296

97+
/**
98+
* @throws ZipValidationException
99+
*/
93100
public function runImport(Import $import, ?string $parent = null)
94101
{
95-
// TODO - Download import zip (if needed)
96-
// TODO - Validate zip file again
97-
// TODO - Check permissions before (create for main item, create for children, create for related items [image, attachments])
102+
$parentModel = null;
103+
if ($import->type === 'page' || $import->type === 'chapter') {
104+
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
105+
}
106+
107+
return $this->importer->run($import, $parentModel);
98108
}
99109

100110
public function deleteImport(Import $import): void

app/Exports/ZipExports/ZipExportValidator.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,19 @@
1010
class ZipExportValidator
1111
{
1212
public function __construct(
13-
protected string $zipPath,
13+
protected ZipExportReader $reader,
1414
) {
1515
}
1616

1717
public function validate(): array
1818
{
19-
$reader = new ZipExportReader($this->zipPath);
2019
try {
21-
$importData = $reader->readData();
20+
$importData = $this->reader->readData();
2221
} catch (ZipExportException $exception) {
2322
return ['format' => $exception->getMessage()];
2423
}
2524

26-
$helper = new ZipValidationHelper($reader);
25+
$helper = new ZipValidationHelper($this->reader);
2726

2827
if (isset($importData['book'])) {
2928
$modelErrors = ZipExportBook::validate($helper, $importData['book']);
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
namespace BookStack\Exports\ZipExports;
4+
5+
use BookStack\Entities\Models\Book;
6+
use BookStack\Entities\Models\Chapter;
7+
use BookStack\Entities\Models\Entity;
8+
use BookStack\Exceptions\ZipExportException;
9+
use BookStack\Exceptions\ZipImportException;
10+
use BookStack\Exports\Import;
11+
use BookStack\Exports\ZipExports\Models\ZipExportBook;
12+
use BookStack\Exports\ZipExports\Models\ZipExportChapter;
13+
use BookStack\Exports\ZipExports\Models\ZipExportPage;
14+
use BookStack\Uploads\FileStorage;
15+
16+
class ZipImportRunner
17+
{
18+
public function __construct(
19+
protected FileStorage $storage,
20+
) {
21+
}
22+
23+
/**
24+
* @throws ZipImportException
25+
*/
26+
public function run(Import $import, ?Entity $parent = null): void
27+
{
28+
$zipPath = $this->getZipPath($import);
29+
$reader = new ZipExportReader($zipPath);
30+
31+
$errors = (new ZipExportValidator($reader))->validate();
32+
if ($errors) {
33+
throw new ZipImportException(["ZIP failed to validate"]);
34+
}
35+
36+
try {
37+
$exportModel = $reader->decodeDataToExportModel();
38+
} catch (ZipExportException $e) {
39+
throw new ZipImportException([$e->getMessage()]);
40+
}
41+
42+
// Validate parent type
43+
if ($exportModel instanceof ZipExportBook && ($parent !== null)) {
44+
throw new ZipImportException(["Must not have a parent set for a Book import"]);
45+
} else if ($exportModel instanceof ZipExportChapter && (!$parent instanceof Book)) {
46+
throw new ZipImportException(["Parent book required for chapter import"]);
47+
} else if ($exportModel instanceof ZipExportPage && !($parent instanceof Book || $parent instanceof Chapter)) {
48+
throw new ZipImportException(["Parent book or chapter required for page import"]);
49+
}
50+
51+
$this->ensurePermissionsPermitImport($exportModel);
52+
53+
// TODO - Run import
54+
}
55+
56+
/**
57+
* @throws ZipImportException
58+
*/
59+
protected function ensurePermissionsPermitImport(ZipExportPage|ZipExportChapter|ZipExportBook $exportModel, Book|Chapter|null $parent = null): void
60+
{
61+
$errors = [];
62+
63+
// TODO - Extract messages to language files
64+
// TODO - Ensure these are shown to users on failure
65+
66+
$chapters = [];
67+
$pages = [];
68+
$images = [];
69+
$attachments = [];
70+
71+
if ($exportModel instanceof ZipExportBook) {
72+
if (!userCan('book-create-all')) {
73+
$errors[] = 'You are lacking the required permission to create books.';
74+
}
75+
array_push($pages, ...$exportModel->pages);
76+
array_push($chapters, ...$exportModel->chapters);
77+
} else if ($exportModel instanceof ZipExportChapter) {
78+
$chapters[] = $exportModel;
79+
} else if ($exportModel instanceof ZipExportPage) {
80+
$pages[] = $exportModel;
81+
}
82+
83+
foreach ($chapters as $chapter) {
84+
array_push($pages, ...$chapter->pages);
85+
}
86+
87+
if (count($chapters) > 0) {
88+
$permission = 'chapter-create' . ($parent ? '' : '-all');
89+
if (!userCan($permission, $parent)) {
90+
$errors[] = 'You are lacking the required permission to create chapters.';
91+
}
92+
}
93+
94+
foreach ($pages as $page) {
95+
array_push($attachments, ...$page->attachments);
96+
array_push($images, ...$page->images);
97+
}
98+
99+
if (count($pages) > 0) {
100+
if ($parent) {
101+
if (!userCan('page-create', $parent)) {
102+
$errors[] = 'You are lacking the required permission to create pages.';
103+
}
104+
} else {
105+
$hasPermission = userCan('page-create-all') || userCan('page-create-own');
106+
if (!$hasPermission) {
107+
$errors[] = 'You are lacking the required permission to create pages.';
108+
}
109+
}
110+
}
111+
112+
if (count($images) > 0) {
113+
if (!userCan('image-create-all')) {
114+
$errors[] = 'You are lacking the required permissions to create images.';
115+
}
116+
}
117+
118+
if (count($attachments) > 0) {
119+
if (userCan('attachment-create-all')) {
120+
$errors[] = 'You are lacking the required permissions to create attachments.';
121+
}
122+
}
123+
124+
if (count($errors)) {
125+
throw new ZipImportException($errors);
126+
}
127+
}
128+
129+
protected function getZipPath(Import $import): string
130+
{
131+
if (!$this->storage->isRemote()) {
132+
return $this->storage->getSystemPath($import->path);
133+
}
134+
135+
$tempFilePath = tempnam(sys_get_temp_dir(), 'bszip-import-');
136+
$tempFile = fopen($tempFilePath, 'wb');
137+
$stream = $this->storage->getReadStream($import->path);
138+
stream_copy_to_stream($stream, $tempFile);
139+
fclose($tempFile);
140+
141+
return $tempFilePath;
142+
}
143+
}

app/Uploads/FileStorage.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\Exceptions\FileUploadException;
66
use Exception;
77
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
8+
use Illuminate\Filesystem\FilesystemAdapter;
89
use Illuminate\Filesystem\FilesystemManager;
910
use Illuminate\Support\Facades\Log;
1011
use Illuminate\Support\Str;
@@ -70,6 +71,26 @@ public function uploadFile(UploadedFile $file, string $subDirectory, string $suf
7071
return $filePath;
7172
}
7273

74+
/**
75+
* Check whether the configured storage is remote from the host of this app.
76+
*/
77+
public function isRemote(): bool
78+
{
79+
return $this->getStorageDiskName() === 's3';
80+
}
81+
82+
/**
83+
* Get the actual path on system for the given relative file path.
84+
*/
85+
public function getSystemPath(string $filePath): string
86+
{
87+
if ($this->isRemote()) {
88+
return '';
89+
}
90+
91+
return storage_path('uploads/files/' . ltrim($this->adjustPathForStorageDisk($filePath), '/'));
92+
}
93+
7394
/**
7495
* Get the storage that will be used for storing files.
7596
*/
@@ -83,7 +104,7 @@ protected function getStorageDisk(): Storage
83104
*/
84105
protected function getStorageDiskName(): string
85106
{
86-
$storageType = config('filesystems.attachments');
107+
$storageType = trim(strtolower(config('filesystems.attachments')));
87108

88109
// Change to our secure-attachment disk if any of the local options
89110
// are used to prevent escaping that location.

0 commit comments

Comments
 (0)