diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php index 582fff975..787fd1b27 100644 --- a/app/Exports/Controllers/ImportController.php +++ b/app/Exports/Controllers/ImportController.php @@ -23,9 +23,8 @@ class ImportController extends Controller * Show the view to start a new import, and also list out the existing * in progress imports that are visible to the user. */ - public function start(Request $request) + public function start() { - // TODO - Test visibility access for listed items $imports = $this->imports->getVisibleImports(); $this->setPageTitle(trans('entities.import')); @@ -64,7 +63,6 @@ class ImportController extends Controller */ public function show(int $id) { - // TODO - Test visibility access $import = $this->imports->findVisible($id); $this->setPageTitle(trans('entities.import_continue')); @@ -74,12 +72,23 @@ class ImportController extends Controller ]); } + public function run(int $id) + { + // TODO - Test access/visibility + + $import = $this->imports->findVisible($id); + + // TODO - Run import + // Validate again before + // TODO - Redirect to result + // TOOD - Or redirect back with errors + } + /** * Delete an active pending import from the filesystem and database. */ public function delete(int $id) { - // TODO - Test visibility access $import = $this->imports->findVisible($id); $this->imports->deleteImport($import); diff --git a/app/Exports/Import.php b/app/Exports/Import.php index 520d8ea6c..8400382fd 100644 --- a/app/Exports/Import.php +++ b/app/Exports/Import.php @@ -3,11 +3,14 @@ namespace BookStack\Exports; use BookStack\Activity\Models\Loggable; +use BookStack\Users\Models\User; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; /** + * @property int $id * @property string $path * @property string $name * @property int $size - ZIP size in bytes @@ -17,6 +20,7 @@ use Illuminate\Database\Eloquent\Model; * @property int $created_by * @property Carbon $created_at * @property Carbon $updated_at + * @property User $createdBy */ class Import extends Model implements Loggable { @@ -59,4 +63,9 @@ class Import extends Model implements Loggable { return "({$this->id}) {$this->name}"; } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } } diff --git a/lang/en/entities.php b/lang/en/entities.php index e2d8e47c5..4f5a53004 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -51,7 +51,11 @@ return [ 'import_pending' => 'Pending Imports', 'import_pending_none' => 'No imports have been started.', 'import_continue' => 'Continue Import', + 'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.', 'import_run' => 'Run Import', + 'import_size' => 'Import ZIP Size:', + 'import_uploaded_at' => 'Uploaded:', + 'import_uploaded_by' => 'Uploaded by:', 'import_delete_confirm' => 'Are you sure you want to delete this import?', 'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.', diff --git a/resources/views/exports/import-show.blade.php b/resources/views/exports/import-show.blade.php index 843a05246..ac1b8a45d 100644 --- a/resources/views/exports/import-show.blade.php +++ b/resources/views/exports/import-show.blade.php @@ -6,7 +6,44 @@

{{ trans('entities.import_continue') }}

-
+

{{ trans('entities.import_continue_desc') }}

+ +
+ @php + $type = $import->getType(); + @endphp +
+
+

@icon($type) {{ $import->name }}

+ @if($type === 'book') +

@icon('chapter') {{ trans_choice('entities.x_chapters', $import->chapter_count) }}

+ @endif + @if($type === 'book' || $type === 'chapter') +

@icon('page') {{ trans_choice('entities.x_pages', $import->page_count) }}

+ @endif +
+
+
+ {{ trans('entities.import_size') }} + {{ $import->getSizeString() }} +
+
+ {{ trans('entities.import_uploaded_at') }} + {{ $import->created_at->diffForHumans() }} +
+ @if($import->createdBy) +
+ {{ trans('entities.import_uploaded_by') }} + {{ $import->createdBy->name }} +
+ @endif +
+
+
+ + {{ csrf_field() }}
@@ -23,7 +60,7 @@ - +
diff --git a/routes/web.php b/routes/web.php index c490bb3b3..85f833528 100644 --- a/routes/web.php +++ b/routes/web.php @@ -210,6 +210,7 @@ Route::middleware('auth')->group(function () { Route::get('/import', [ExportControllers\ImportController::class, 'start']); Route::post('/import', [ExportControllers\ImportController::class, 'upload']); Route::get('/import/{id}', [ExportControllers\ImportController::class, 'show']); + Route::post('/import/{id}', [ExportControllers\ImportController::class, 'run']); Route::delete('/import/{id}', [ExportControllers\ImportController::class, 'delete']); // Other Pages diff --git a/tests/Exports/ZipImportTest.php b/tests/Exports/ZipImportTest.php index c9d255b1e..b9a8598fa 100644 --- a/tests/Exports/ZipImportTest.php +++ b/tests/Exports/ZipImportTest.php @@ -2,6 +2,8 @@ namespace Tests\Exports; +use BookStack\Activity\ActivityType; +use BookStack\Exports\Import; use Illuminate\Http\UploadedFile; use Illuminate\Testing\TestResponse; use Tests\TestCase; @@ -35,6 +37,25 @@ class ZipImportTest extends TestCase $resp->assertSeeText('Select ZIP file to upload'); } + public function test_import_page_pending_import_visibility_limited() + { + $user = $this->users->viewer(); + $admin = $this->users->admin(); + $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]); + $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]); + $this->permissions->grantUserRolePermissions($user, ['content-import']); + + $resp = $this->actingAs($user)->get('/import'); + $resp->assertSeeText('MySuperUserImport'); + $resp->assertDontSeeText('MySuperAdminImport'); + + $this->permissions->grantUserRolePermissions($user, ['settings-manage']); + + $resp = $this->actingAs($user)->get('/import'); + $resp->assertSeeText('MySuperUserImport'); + $resp->assertSeeText('MySuperAdminImport'); + } + public function test_zip_read_errors_are_shown_on_validation() { $invalidUpload = $this->files->uploadedImage('image.zip'); @@ -105,6 +126,125 @@ class ZipImportTest extends TestCase $resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.'); } + public function test_import_upload_success() + { + $admin = $this->users->admin(); + $this->actingAs($admin); + $resp = $this->runImportFromFile($this->zipUploadFromData([ + 'book' => [ + 'name' => 'My great book name', + 'chapters' => [ + [ + 'name' => 'my chapter', + 'pages' => [ + [ + 'name' => 'my chapter page', + ] + ] + ] + ], + 'pages' => [ + [ + 'name' => 'My page', + ] + ], + ], + ])); + + $this->assertDatabaseHas('imports', [ + 'name' => 'My great book name', + 'book_count' => 1, + 'chapter_count' => 1, + 'page_count' => 2, + 'created_by' => $admin->id, + ]); + + /** @var Import $import */ + $import = Import::query()->latest()->first(); + $resp->assertRedirect("/import/{$import->id}"); + $this->assertFileExists(storage_path($import->path)); + $this->assertActivityExists(ActivityType::IMPORT_CREATE); + } + + public function test_import_show_page() + { + $import = Import::factory()->create(['name' => 'MySuperAdminImport']); + + $resp = $this->asAdmin()->get("/import/{$import->id}"); + $resp->assertOk(); + $resp->assertSee('MySuperAdminImport'); + } + + public function test_import_show_page_access_limited() + { + $user = $this->users->viewer(); + $admin = $this->users->admin(); + $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]); + $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]); + $this->actingAs($user); + + $this->get("/import/{$userImport->id}")->assertRedirect('/'); + $this->get("/import/{$adminImport->id}")->assertRedirect('/'); + + $this->permissions->grantUserRolePermissions($user, ['content-import']); + + $this->get("/import/{$userImport->id}")->assertOk(); + $this->get("/import/{$adminImport->id}")->assertStatus(404); + + $this->permissions->grantUserRolePermissions($user, ['settings-manage']); + + $this->get("/import/{$userImport->id}")->assertOk(); + $this->get("/import/{$adminImport->id}")->assertOk(); + } + + public function test_import_delete() + { + $this->asAdmin(); + $this->runImportFromFile($this->zipUploadFromData([ + 'book' => [ + 'name' => 'My great book name' + ], + ])); + + /** @var Import $import */ + $import = Import::query()->latest()->first(); + $this->assertDatabaseHas('imports', [ + 'id' => $import->id, + 'name' => 'My great book name' + ]); + $this->assertFileExists(storage_path($import->path)); + + $resp = $this->delete("/import/{$import->id}"); + + $resp->assertRedirect('/import'); + $this->assertActivityExists(ActivityType::IMPORT_DELETE); + $this->assertDatabaseMissing('imports', [ + 'id' => $import->id, + ]); + $this->assertFileDoesNotExist(storage_path($import->path)); + } + + public function test_import_delete_access_limited() + { + $user = $this->users->viewer(); + $admin = $this->users->admin(); + $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]); + $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]); + $this->actingAs($user); + + $this->delete("/import/{$userImport->id}")->assertRedirect('/'); + $this->delete("/import/{$adminImport->id}")->assertRedirect('/'); + + $this->permissions->grantUserRolePermissions($user, ['content-import']); + + $this->delete("/import/{$userImport->id}")->assertRedirect('/import'); + $this->delete("/import/{$adminImport->id}")->assertStatus(404); + + $this->permissions->grantUserRolePermissions($user, ['settings-manage']); + + $this->delete("/import/{$adminImport->id}")->assertRedirect('/import'); + } + protected function runImportFromFile(UploadedFile $file): TestResponse { return $this->call('POST', '/import', [], [], ['file' => $file]);