diff --git a/i18n/translations/en.json b/i18n/translations/en.json index 88f61600f9..7b592ce44e 100644 --- a/i18n/translations/en.json +++ b/i18n/translations/en.json @@ -103,9 +103,12 @@ "error.form.notSaved": "The form could not be saved", "error.language.code": "Please enter a valid code for the language", + "error.language.create.permission": "You are not allowed to create a language", + "error.language.delete.permission": "You are not allowed to delete the language", "error.language.duplicate": "The language already exists", "error.language.name": "Please enter a valid name for the language", "error.language.notFound": "The language could not be found", + "error.language.update.permission": "You are not allowed to update the language", "error.layout.validation.block": "There's an error on the \"{field}\" field in block {blockIndex} using the \"{fieldset}\" block type in layout {layoutIndex}", "error.layout.validation.settings": "There's an error in layout {index} settings", diff --git a/panel/src/components/Views/LanguagesView.vue b/panel/src/components/Views/LanguagesView.vue index 83075936d6..8c72c7004b 100644 --- a/panel/src/components/Views/LanguagesView.vue +++ b/panel/src/components/Views/LanguagesView.vue @@ -7,6 +7,7 @@ @@ -30,14 +31,23 @@ v-if="secondaryLanguages.length" :items="secondaryLanguages" /> - + {{ $t("languages.secondary.empty") }} @@ -66,12 +76,17 @@ export default { icon: "globe" }, link: () => { + if (!this.$permissions.languages.update) { + return null; + } + this.$dialog(`languages/${language.id}/update`); }, options: [ { icon: "edit", text: this.$t("edit"), + disabled: !this.$permissions.languages.update, click() { this.$dialog(`languages/${language.id}/update`); } @@ -79,7 +94,10 @@ export default { { icon: "trash", text: this.$t("delete"), - disabled: language.default && this.languages.length !== 1, + disabled: ( + (language.default && this.languages.length !== 1) || + !this.$permissions.languages.delete + ), click() { this.$dialog(`languages/${language.id}/delete`); } diff --git a/src/Cms/Language.php b/src/Cms/Language.php index 42e95c4f65..52b8c66642 100644 --- a/src/Cms/Language.php +++ b/src/Cms/Language.php @@ -201,8 +201,17 @@ protected static function converter(string $from, string $to): bool */ public static function create(array $props) { + $kirby = App::instance(); + $user = $kirby->user(); + + if ( + $user === null || + $user->role()->permissions()->for('languages', 'create') === false + ) { + throw new PermissionException(['key' => 'language.create.permission']); + } + $props['code'] = Str::slug($props['code'] ?? null); - $kirby = App::instance(); $languages = $kirby->languages(); // make the first language the default language @@ -256,10 +265,18 @@ public static function create(array $props) public function delete(): bool { $kirby = App::instance(); + $user = $kirby->user(); $languages = $kirby->languages(); $code = $this->code(); $isLast = $languages->count() === 1; + if ( + $user === null || + $user->role()->permissions()->for('languages', 'delete') === false + ) { + throw new PermissionException(['key' => 'language.delete.permission']); + } + // trigger before hook $kirby->trigger('language.delete:before', [ 'language' => $this @@ -676,13 +693,22 @@ public function url(): string */ public function update(array $props = null) { + $kirby = App::instance(); + $user = $kirby->user(); + + if ( + $user === null || + $user->role()->permissions()->for('languages', 'update') === false + ) { + throw new PermissionException(['key' => 'language.update.permission']); + } + // don't change the language code unset($props['code']); // make sure the slug is nice and clean $props['slug'] = Str::slug($props['slug'] ?? null); - $kirby = App::instance(); $updated = $this->clone($props); // validate the updated language diff --git a/src/Cms/Permissions.php b/src/Cms/Permissions.php index 2780a84b41..c9b2362555 100644 --- a/src/Cms/Permissions.php +++ b/src/Cms/Permissions.php @@ -44,7 +44,8 @@ class Permissions ], 'languages' => [ 'create' => true, - 'delete' => true + 'delete' => true, + 'update' => true ], 'pages' => [ 'changeSlug' => true, diff --git a/tests/Cms/Languages/LanguageTest.php b/tests/Cms/Languages/LanguageTest.php index a8445a6559..31e866825e 100644 --- a/tests/Cms/Languages/LanguageTest.php +++ b/tests/Cms/Languages/LanguageTest.php @@ -4,6 +4,7 @@ use Kirby\Data\Data; use Kirby\Exception\InvalidArgumentException; +use Kirby\Exception\PermissionException; use Kirby\Filesystem\Dir; use Kirby\Filesystem\F; use PHPUnit\Framework\TestCase; @@ -472,6 +473,8 @@ public function testBaseUrl($kirbyUrl, $url, $expected) public function testCreate() { + $this->app->impersonate('kirby'); + $language = Language::create([ 'code' => 'en' ]); @@ -485,6 +488,8 @@ public function testCreate() public function testDelete() { + $this->app->impersonate('kirby'); + $language = Language::create([ 'code' => 'en' ]); @@ -496,6 +501,8 @@ public function testUpdate() { Dir::make($contentDir = $this->fixtures . '/content'); + $this->app->impersonate('kirby'); + $language = Language::create([ 'code' => 'en' ]); @@ -505,12 +512,55 @@ public function testUpdate() $this->assertSame('English', $language->name()); } + /** + * @covers ::create + */ + public function testCreateNoPermissions() + { + $app = $this->app->clone([ + 'blueprints' => [ + 'users/editor' => [ + 'name' => 'editor', + 'permissions' => [ + 'languages' => [ + 'create' => false + ] + ] + ], + ], + 'users' => [ + ['email' => 'test@getkirby.com', 'role' => 'editor'] + ] + ]); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to create a language'); + + $app->impersonate('test@getkirby.com'); + Language::create([ + 'code' => 'en' + ]); + } + + /** + * @covers ::create + */ + public function testCreateWithoutLoggedUser() + { + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to create a language'); + + Language::create([ + 'code' => 'en' + ]); + } + public function testCreateHooks() { $calls = 0; $phpunit = $this; - new App([ + $app = $this->app->clone([ 'roots' => [ 'index' => $this->fixtures = __DIR__ . '/fixtures/CreateHooksTest', ], @@ -530,6 +580,8 @@ public function testCreateHooks() ] ]); + $app->impersonate('kirby'); + Language::create([ 'code' => 'de' ]); @@ -545,7 +597,7 @@ public function testUpdateHooks() $this->fixtures = __DIR__ . '/fixtures/UpdateHooksTest'; Dir::make($this->fixtures . '/content'); - new App([ + $app = $this->app->clone([ 'roots' => [ 'index' => $this->fixtures, ], @@ -573,18 +625,113 @@ public function testUpdateHooks() ] ]); + $app->impersonate('kirby'); + $language = Language::create(['code' => 'en']); $language->update(['name' => 'English']); $this->assertSame(2, $calls); } + /** + * @covers ::update + */ + public function testUpdateNoPermissions() + { + $app = $this->app->clone([ + 'blueprints' => [ + 'users/editor' => [ + 'name' => 'editor', + 'permissions' => [ + 'languages' => [ + 'create' => true, + 'update' => false + ] + ] + ], + ], + 'users' => [ + ['email' => 'test@getkirby.com', 'role' => 'editor'] + ] + ]); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to update the language'); + + $app->impersonate('test@getkirby.com'); + + $language = Language::create(['code' => 'en']); + $language->update(['name' => 'English']); + } + + /** + * @covers ::update + */ + public function testUpdateWithoutLoggedUser() + { + $this->app->impersonate('kirby'); + $language = Language::create(['code' => 'en']); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to update the language'); + + // unimpersonate and test the method + $this->app->impersonate(); + $language->update(['name' => 'English']); + } + + /** + * @covers ::delete + */ + public function testDeleteNoPermissions() + { + $app = $this->app->clone([ + 'blueprints' => [ + 'users/editor' => [ + 'name' => 'editor', + 'permissions' => [ + 'languages' => [ + 'create' => true, + 'delete' => false + ] + ] + ], + ], + 'users' => [ + ['email' => 'test@getkirby.com', 'role' => 'editor'] + ] + ]); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to delete the language'); + + $app->impersonate('test@getkirby.com'); + $language = Language::create(['code' => 'en']); + $language->delete(); + } + + /** + * @covers ::delete + */ + public function testDeleteWithoutLoggedUser() + { + $this->app->impersonate('kirby'); + $language = Language::create(['code' => 'en']); + + $this->expectException(PermissionException::class); + $this->expectExceptionMessage('You are not allowed to delete the language'); + + // unimpersonate and test the method + $this->app->impersonate(); + $language->delete(); + } + public function testDeleteHooks() { $calls = 0; $phpunit = $this; - new App([ + $app =new App([ 'roots' => [ 'index' => $this->fixtures = __DIR__ . '/fixtures/DeleteHooksTest', ], @@ -604,6 +751,8 @@ public function testDeleteHooks() ] ]); + $app->impersonate('kirby'); + $language = Language::create([ 'code' => 'en', 'name' => 'English' diff --git a/tests/Cms/Languages/LanguagesTest.php b/tests/Cms/Languages/LanguagesTest.php index c720e517ed..df87cac5e6 100644 --- a/tests/Cms/Languages/LanguagesTest.php +++ b/tests/Cms/Languages/LanguagesTest.php @@ -108,6 +108,8 @@ public function testMultipleDefault() public function testCreate() { + $this->app->impersonate('kirby'); + $language = $this->app->languages()->create([ 'code' => 'tr' ]);