From 042841c199d0534b471ccfcb662ec39a64c0de16 Mon Sep 17 00:00:00 2001 From: Josh Heyer Date: Wed, 8 Feb 2023 11:51:03 -0700 Subject: [PATCH 01/44] Whoops: Allow variable masking with blacklist --- src/Cms/AppErrors.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Cms/AppErrors.php b/src/Cms/AppErrors.php index af7e916bbb..3ba08ba7aa 100644 --- a/src/Cms/AppErrors.php +++ b/src/Cms/AppErrors.php @@ -82,6 +82,14 @@ protected function handleHtmlErrors(): void if ($editor = $this->option('editor')) { $handler->setEditor($editor); } + + if ($blacklist = $this->option('whoops_blacklist')) { + foreach($blacklist as $superglobal => $vars) { + foreach($vars as $var) { + $handler->blacklist($superglobal, $var); + } + } + } } } else { $handler = new CallbackHandler(function ($exception, $inspector, $run) { From 5aaef66aec9c64fd287416c8c86f4603516c154b Mon Sep 17 00:00:00 2001 From: HYR Date: Fri, 21 Jul 2023 20:49:14 -0600 Subject: [PATCH 02/44] Change option to use dot notation --- src/Cms/AppErrors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cms/AppErrors.php b/src/Cms/AppErrors.php index 3ba08ba7aa..e6c3640bd9 100644 --- a/src/Cms/AppErrors.php +++ b/src/Cms/AppErrors.php @@ -83,7 +83,7 @@ protected function handleHtmlErrors(): void $handler->setEditor($editor); } - if ($blacklist = $this->option('whoops_blacklist')) { + if ($blacklist = $this->option('whoops.blacklist')) { foreach($blacklist as $superglobal => $vars) { foreach($vars as $var) { $handler->blacklist($superglobal, $var); From 94bdf219365606b5b57f484b31751a299cc283e1 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Sat, 22 Jul 2023 13:53:46 +0200 Subject: [PATCH 03/44] Fix coding style --- src/Cms/AppErrors.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Cms/AppErrors.php b/src/Cms/AppErrors.php index e6c3640bd9..cae5e19857 100644 --- a/src/Cms/AppErrors.php +++ b/src/Cms/AppErrors.php @@ -83,13 +83,13 @@ protected function handleHtmlErrors(): void $handler->setEditor($editor); } - if ($blacklist = $this->option('whoops.blacklist')) { - foreach($blacklist as $superglobal => $vars) { - foreach($vars as $var) { - $handler->blacklist($superglobal, $var); - } - } - } + if ($blacklist = $this->option('whoops.blacklist')) { + foreach ($blacklist as $superglobal => $vars) { + foreach ($vars as $var) { + $handler->blacklist($superglobal, $var); + } + } + } } } else { $handler = new CallbackHandler(function ($exception, $inspector, $run) { From cdfbd34b72f28ba9016214ef94359d9d442a5668 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Sat, 22 Jul 2023 13:54:25 +0200 Subject: [PATCH 04/44] Rename option to `whoops.blocklist` --- src/Cms/AppErrors.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cms/AppErrors.php b/src/Cms/AppErrors.php index cae5e19857..bfbf58941e 100644 --- a/src/Cms/AppErrors.php +++ b/src/Cms/AppErrors.php @@ -83,8 +83,8 @@ protected function handleHtmlErrors(): void $handler->setEditor($editor); } - if ($blacklist = $this->option('whoops.blacklist')) { - foreach ($blacklist as $superglobal => $vars) { + if ($blocklist = $this->option('whoops.blocklist')) { + foreach ($blocklist as $superglobal => $vars) { foreach ($vars as $var) { $handler->blacklist($superglobal, $var); } From 9e8ae734e6b4c92545ef3c3ae9d01529e2c5e27f Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Sat, 22 Jul 2023 13:58:13 +0200 Subject: [PATCH 05/44] Fix compatibility with nested whoops option --- src/Cms/AppErrors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cms/AppErrors.php b/src/Cms/AppErrors.php index bfbf58941e..f61d02b47e 100644 --- a/src/Cms/AppErrors.php +++ b/src/Cms/AppErrors.php @@ -73,7 +73,7 @@ protected function handleHtmlErrors(): void $handler = null; if ($this->option('debug') === true) { - if ($this->option('whoops', true) === true) { + if ($this->option('whoops', true) !== false) { $handler = new PrettyPageHandler(); $handler->setPageTitle('Kirby CMS Debugger'); $handler->setResourcesPath(dirname(__DIR__, 2) . '/assets'); From 45779efe508b893d95cd5a7ba81cb1ba60970129 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Tue, 25 Jul 2023 13:28:08 +0200 Subject: [PATCH 06/44] Text field: use `text` input if no specific input --- panel/src/components/Forms/Field/TextField.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/panel/src/components/Forms/Field/TextField.vue b/panel/src/components/Forms/Field/TextField.vue index be608cba37..d2ee84ede7 100644 --- a/panel/src/components/Forms/Field/TextField.vue +++ b/panel/src/components/Forms/Field/TextField.vue @@ -12,6 +12,7 @@ v-bind="$props" :id="_uid" ref="input" + :type="inputType" theme="field" v-on="$listeners" /> @@ -32,6 +33,15 @@ import counter from "@/mixins/forms/counter.js"; export default { mixins: [Field, Input, TextInput, counter], inheritAttrs: false, + computed: { + inputType() { + if (this.$helper.isComponent(`k-${this.type}-input`)) { + return this.type; + } + + return "text"; + } + }, methods: { focus() { this.$refs.input.focus(); From e92ea8f5148fa972cd81dacbac9108124e471d0e Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Thu, 27 Jul 2023 21:52:24 +0200 Subject: [PATCH 07/44] Fix session error in CI --- tests/Cms/Api/routes/AccountRoutesTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Cms/Api/routes/AccountRoutesTest.php b/tests/Cms/Api/routes/AccountRoutesTest.php index 5e06417dd9..5e758be317 100644 --- a/tests/Cms/Api/routes/AccountRoutesTest.php +++ b/tests/Cms/Api/routes/AccountRoutesTest.php @@ -48,6 +48,7 @@ public function setUp(): void public function tearDown(): void { + $this->app->session()->destroy(); App::destroy(); Field::$types = []; Section::$types = []; From 752b05e36e2e6eefcba880887a14bc062125681b Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Thu, 27 Jul 2023 21:52:45 +0200 Subject: [PATCH 08/44] More PHPUnit test cleanup for better reliability --- tests/Cms/Users/UserActionsTest.php | 31 +++++++++++++++-------------- tests/Cms/Users/UserAuthTest.php | 6 ++++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/Cms/Users/UserActionsTest.php b/tests/Cms/Users/UserActionsTest.php index c9c803dd45..237de09fd9 100644 --- a/tests/Cms/Users/UserActionsTest.php +++ b/tests/Cms/Users/UserActionsTest.php @@ -44,6 +44,7 @@ public function tearDown(): void { $this->app->session()->destroy(); Dir::remove($this->tmp); + App::destroy(); } public function testChangeEmail() @@ -303,7 +304,7 @@ public function testChangeEmailHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.changeEmail:before' => function (User $user, $email) use ($phpunit, &$calls) { $phpunit->assertSame('editor@domain.com', $user->email()); @@ -318,7 +319,7 @@ public function testChangeEmailHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->changeEmail('another@domain.com'); $this->assertSame(2, $calls); @@ -329,7 +330,7 @@ public function testChangeLanguageHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.changeLanguage:before' => function (User $user, $language) use ($phpunit, &$calls) { $phpunit->assertSame('en', $user->language()); @@ -344,7 +345,7 @@ public function testChangeLanguageHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->changeLanguage('de'); $this->assertSame(2, $calls); @@ -355,7 +356,7 @@ public function testChangeNameHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.changeName:before' => function (User $user, $name) use ($phpunit, &$calls) { $phpunit->assertNull($user->name()->value()); @@ -370,7 +371,7 @@ public function testChangeNameHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->changeName('Edith Thor'); $this->assertSame(2, $calls); @@ -381,7 +382,7 @@ public function testChangePasswordHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.changePassword:before' => function (User $user, $password) use ($phpunit, &$calls) { $phpunit->assertEmpty($user->password()); @@ -402,7 +403,7 @@ public function testChangePasswordHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->changePassword('topsecret2018'); $this->assertSame(3, $calls); @@ -445,7 +446,7 @@ public function testChangeRoleHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.changeRole:before' => function (User $user, $role) use ($phpunit, &$calls) { $phpunit->assertSame('editor', $user->role()->name()); @@ -460,7 +461,7 @@ public function testChangeRoleHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->changeRole('admin'); $this->assertSame(2, $calls); @@ -476,7 +477,7 @@ public function testCreateHooks() 'model' => 'admin', ]; - $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.create:before' => function (User $user, $input) use ($phpunit, $userInput, &$calls) { $phpunit->assertSame('new@domain.com', $user->email()); @@ -502,7 +503,7 @@ public function testDeleteHooks() $calls = 0; $phpunit = $this; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.delete:before' => function (User $user) use ($phpunit, &$calls) { $phpunit->assertSame('editor@domain.com', $user->email()); @@ -518,7 +519,7 @@ public function testDeleteHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->delete(); $this->assertSame(2, $calls); @@ -532,7 +533,7 @@ public function testUpdateHooks() 'website' => 'https://getkirby.com' ]; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.update:before' => function (User $user, $values, $strings) use ($phpunit, $input, &$calls) { $phpunit->assertNull($user->website()->value()); @@ -548,7 +549,7 @@ public function testUpdateHooks() ] ]); - $user = $app->user('editor@domain.com'); + $user = $this->app->user('editor@domain.com'); $user->update($input); $this->assertSame(2, $calls); diff --git a/tests/Cms/Users/UserAuthTest.php b/tests/Cms/Users/UserAuthTest.php index 6bfde4e5f7..23204cb8a1 100644 --- a/tests/Cms/Users/UserAuthTest.php +++ b/tests/Cms/Users/UserAuthTest.php @@ -30,7 +30,9 @@ public function setUp(): void public function tearDown(): void { + $this->app->session()->destroy(); Dir::remove($this->tmp); + App::destroy(); } public function testGlobalUserState() @@ -50,7 +52,7 @@ public function testLoginLogoutHooks() $calls = 0; $logoutSession = false; - $app = $this->app->clone([ + $this->app = $this->app->clone([ 'hooks' => [ 'user.login:before' => function ($user, $session) use ($phpunit, &$calls) { $phpunit->assertSame('test@getkirby.com', $user->email()); @@ -86,7 +88,7 @@ public function testLoginLogoutHooks() ]); // without prepopulated session - $user = $app->user('test@getkirby.com'); + $user = $this->app->user('test@getkirby.com'); $user->loginPasswordless(); $user->logout(); From 3675faba7763cfc2a57df5b85c666a04b7ddf438 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Thu, 27 Jul 2023 21:53:00 +0200 Subject: [PATCH 09/44] FTest::testMoveAcrossDevices(): Test in CI --- tests/Filesystem/FTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/Filesystem/FTest.php b/tests/Filesystem/FTest.php index adc5140423..8297141770 100644 --- a/tests/Filesystem/FTest.php +++ b/tests/Filesystem/FTest.php @@ -493,7 +493,15 @@ public function testMove() */ public function testMoveAcrossDevices() { - $tmpDir = sys_get_temp_dir(); + // try to find a suitable path on a different device (filesystem) + if (is_dir('/dev/shm') === true) { + // use tmpfs mount point on GitHub Actions + $tmpDir = '/dev/shm'; + } else { + // no luck, try the system temp dir, + // which often also uses tmpfs + $tmpDir = sys_get_temp_dir(); + } if (stat($this->tmp)['dev'] === stat($tmpDir)['dev']) { $this->markTestSkipped('Temporary directory "' . $tmpDir . '" is on the same filesystem'); From ab7efda82866959ee0d4aebadafd47f2a22dcd32 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Thu, 27 Jul 2023 22:01:00 +0200 Subject: [PATCH 10/44] Fix coverage reporting --- tests/Cms/Auth/AuthTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Cms/Auth/AuthTest.php b/tests/Cms/Auth/AuthTest.php index 7a81a09696..3ffea737e5 100644 --- a/tests/Cms/Auth/AuthTest.php +++ b/tests/Cms/Auth/AuthTest.php @@ -305,8 +305,7 @@ public function testUserSessionManualSession() } /** - * @covers ::status - * @covers ::user + * @covers ::currentUserFromSession */ public function testUserSessionOldTimestamp() { @@ -326,8 +325,7 @@ public function testUserSessionOldTimestamp() } /** - * @covers ::status - * @covers ::user + * @covers ::currentUserFromSession */ public function testUserSessionNoTimestamp() { From 112acb7375ac032e889558c9c8bdbc184dc15566 Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Sat, 29 Jul 2023 10:07:12 +0300 Subject: [PATCH 11/44] Fix nullable search query #5428 --- config/components.php | 12 +++++++++--- tests/Cms/Collections/SearchTest.php | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/components.php b/config/components.php index 135ceef91d..5330e86564 100644 --- a/config/components.php +++ b/config/components.php @@ -140,9 +140,16 @@ 'search' => function ( App $kirby, Collection $collection, - string $query = '', - $params = [] + string $query = null, + array|string $params = [] ): Collection|bool { + $query = trim($query ?? ''); + + // empty search query + if (empty($query) === true) { + return $collection->limit(0); + } + if (is_string($params) === true) { $params = ['fields' => Str::split($params, '|')]; } @@ -156,7 +163,6 @@ $collection = clone $collection; $options = array_merge($defaults, $params); - $query = trim($query); // empty or too short search query if (Str::length($query) < $options['minlength']) { diff --git a/tests/Cms/Collections/SearchTest.php b/tests/Cms/Collections/SearchTest.php index 136b19a8bb..4435e2f58c 100644 --- a/tests/Cms/Collections/SearchTest.php +++ b/tests/Cms/Collections/SearchTest.php @@ -39,6 +39,12 @@ public function testCollection() $search = Search::collection($collection, ' '); $this->assertCount(0, $search); + + $search = Search::collection($collection, null); + $this->assertCount(0, $search); + + $search = Search::collection($collection); + $this->assertCount(0, $search); } From b1db0e23f7df051c41baebce0b75a3764375fa8b Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Sat, 29 Jul 2023 22:35:26 +0200 Subject: [PATCH 12/44] New `panel.frameAncestors` option --- src/Panel/Document.php | 10 +++- tests/Panel/DocumentTest.php | 93 ++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/Panel/Document.php b/src/Panel/Document.php index 4d9d026d53..ee2a62d8a7 100644 --- a/src/Panel/Document.php +++ b/src/Panel/Document.php @@ -278,8 +278,16 @@ public static function response(array $fiber): Response 'panelUrl' => $uri->path()->toString(true) . '/', ]); + $frameAncestors = $kirby->option('panel.frameAncestors'); + $frameAncestors = match (true) { + $frameAncestors === true => "'self'", + is_array($frameAncestors) => "'self' " . implode(' ', $frameAncestors), + is_string($frameAncestors) => $frameAncestors, + default => "'none'" + }; + return new Response($body, 'text/html', $code, [ - 'Content-Security-Policy' => "frame-ancestors 'none'" + 'Content-Security-Policy' => 'frame-ancestors ' . $frameAncestors ]); } } diff --git a/tests/Panel/DocumentTest.php b/tests/Panel/DocumentTest.php index fd4afe548b..f9d2e42d77 100644 --- a/tests/Panel/DocumentTest.php +++ b/tests/Panel/DocumentTest.php @@ -361,4 +361,97 @@ public function testResponse(): void $this->assertSame("frame-ancestors 'none'", $response->header('Content-Security-Policy')); $this->assertNotNull($response->body()); } + + /** + * @covers ::response + */ + public function testResponseFrameAncestorsSelf(): void + { + $this->app = $this->app->clone([ + 'options' => [ + 'panel' => [ + 'frameAncestors' => true + ] + ] + ]); + + // create panel dist files first to avoid redirect + Document::link($this->app); + + // get panel response + $response = Document::response([ + 'test' => 'Test' + ]); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->code()); + $this->assertSame('text/html', $response->type()); + $this->assertSame('UTF-8', $response->charset()); + $this->assertSame("frame-ancestors 'self'", $response->header('Content-Security-Policy')); + $this->assertNotNull($response->body()); + } + + /** + * @covers ::response + */ + public function testResponseFrameAncestorsArray(): void + { + $this->app = $this->app->clone([ + 'options' => [ + 'panel' => [ + 'frameAncestors' => ['*.example.com', 'https://example.com'] + ] + ] + ]); + + // create panel dist files first to avoid redirect + Document::link($this->app); + + // get panel response + $response = Document::response([ + 'test' => 'Test' + ]); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->code()); + $this->assertSame('text/html', $response->type()); + $this->assertSame('UTF-8', $response->charset()); + $this->assertSame( + "frame-ancestors 'self' *.example.com https://example.com", + $response->header('Content-Security-Policy') + ); + $this->assertNotNull($response->body()); + } + + /** + * @covers ::response + */ + public function testResponseFrameAncestorsString(): void + { + $this->app = $this->app->clone([ + 'options' => [ + 'panel' => [ + 'frameAncestors' => '*.example.com https://example.com' + ] + ] + ]); + + // create panel dist files first to avoid redirect + Document::link($this->app); + + // get panel response + $response = Document::response([ + 'test' => 'Test' + ]); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->code()); + $this->assertSame('text/html', $response->type()); + $this->assertSame('UTF-8', $response->charset()); + $this->assertSame( + 'frame-ancestors *.example.com https://example.com', + $response->header('Content-Security-Policy') + ); + $this->assertNotNull($response->body()); + } } From 06e2b5636d5d0f1dd6282b833b014610b08c0a1a Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Mon, 31 Jul 2023 00:26:44 +0300 Subject: [PATCH 13/44] Simplify search component #5428 --- config/components.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/config/components.php b/config/components.php index 5330e86564..3717bf5875 100644 --- a/config/components.php +++ b/config/components.php @@ -140,16 +140,9 @@ 'search' => function ( App $kirby, Collection $collection, - string $query = null, + string|null $query = '', array|string $params = [] ): Collection|bool { - $query = trim($query ?? ''); - - // empty search query - if (empty($query) === true) { - return $collection->limit(0); - } - if (is_string($params) === true) { $params = ['fields' => Str::split($params, '|')]; } @@ -161,8 +154,9 @@ 'words' => false, ]; - $collection = clone $collection; - $options = array_merge($defaults, $params); + $collection = clone $collection; + $options = array_merge($defaults, $params); + $query = trim($query ?? ''); // empty or too short search query if (Str::length($query) < $options['minlength']) { From 44386cb312e6c5c7841721791ea29a0cfac36585 Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Mon, 31 Jul 2023 11:33:17 +0300 Subject: [PATCH 14/44] Remove unnecessary cloning #5428 Co-authored-by: Lukas Bestle --- config/components.php | 1 - 1 file changed, 1 deletion(-) diff --git a/config/components.php b/config/components.php index 3717bf5875..b13d94c009 100644 --- a/config/components.php +++ b/config/components.php @@ -154,7 +154,6 @@ 'words' => false, ]; - $collection = clone $collection; $options = array_merge($defaults, $params); $query = trim($query ?? ''); From 761efcb8c2e300fe16a8e267679fd62c3862ba11 Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Sun, 30 Jul 2023 03:10:03 +0300 Subject: [PATCH 15/44] Re-populate when invalid uuid cached #5430 --- src/Uuid/SiteUuid.php | 2 +- src/Uuid/UserUuid.php | 2 +- src/Uuid/Uuid.php | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Uuid/SiteUuid.php b/src/Uuid/SiteUuid.php index 4d3a28dbd9..bbe352038e 100644 --- a/src/Uuid/SiteUuid.php +++ b/src/Uuid/SiteUuid.php @@ -46,7 +46,7 @@ public function model(bool $lazy = false): Site /** * Pretends to fill cache - we don't need it in cache */ - public function populate(): bool + public function populate(bool $force = false): bool { return true; } diff --git a/src/Uuid/UserUuid.php b/src/Uuid/UserUuid.php index a071f69bb6..f32fec1f7a 100644 --- a/src/Uuid/UserUuid.php +++ b/src/Uuid/UserUuid.php @@ -46,7 +46,7 @@ public function model(bool $lazy = false): User|null /** * Pretends to fill cache - we don't need it in cache */ - public function populate(): bool + public function populate(bool $force = false): bool { return true; } diff --git a/src/Uuid/Uuid.php b/src/Uuid/Uuid.php index 4b43099285..b3e6cf6acf 100644 --- a/src/Uuid/Uuid.php +++ b/src/Uuid/Uuid.php @@ -309,7 +309,8 @@ public function model(bool $lazy = false): Identifiable|null // lazily fill cache by writing to cache // whenever looked up from index to speed // up future lookups of the same UUID - $this->populate(); + // also force to update value again if it is already cached + $this->populate($this->isCached()); return $this->model; } @@ -320,12 +321,10 @@ public function model(bool $lazy = false): Identifiable|null /** * Feeds the UUID into the cache - * - * @return bool */ - public function populate(): bool + public function populate(bool $force = false): bool { - if ($this->isCached() === true) { + if ($force === false && $this->isCached() === true) { return true; } From 12a33472bd16e7228563d423e804bf8a9f66a23f Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Sun, 30 Jul 2023 13:31:09 +0300 Subject: [PATCH 16/44] Add unit test for caching invalid model id #5430 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Nico Hoffmann ෴. <3788865+distantnative@users.noreply.github.com> --- tests/Uuid/UuidTest.php | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/Uuid/UuidTest.php b/tests/Uuid/UuidTest.php index e71da111e9..d6497473f0 100644 --- a/tests/Uuid/UuidTest.php +++ b/tests/Uuid/UuidTest.php @@ -3,7 +3,6 @@ namespace Kirby\Uuid; use Generator; -use Kirby\Cms\Page; use Kirby\Exception\LogicException; use Kirby\Toolkit\Str; @@ -377,4 +376,33 @@ public function testValue() $page = $this->app->page($dir = 'page-a/subpage-a'); $this->assertSame($dir, $page->uuid()->value()); } + + /** + * @covers ::model + * @covers ::populate + */ + public function testCacheInvalidModelId() + { + $page = $this->app->page('page-a'); + $key = 'page/my/-page'; + $id = 'page://my-page'; + $uuid = Uuid::for($id); + + $this->assertFalse($uuid->isCached()); + $this->assertSame($key, $uuid->key()); + $this->assertSame($uuid->toString(), $id); + $this->assertTrue($uuid->isCached()); + $this->assertSame(Uuids::cache()->get($key), 'page-a'); + $this->assertSame($page, $this->app->page($id)); + + // modify cache data manually to something invalid + Uuids::cache()->set($key, 'invalid-id'); + + $uuid = Uuid::for($id); + $this->assertSame(Uuids::cache()->get($key), 'invalid-id'); + $this->assertNull($uuid->model(true)); + $this->assertSame(Uuids::cache()->get($key), 'invalid-id'); + $this->assertSame($page, $uuid->model()); + $this->assertSame(Uuids::cache()->get($key), 'page-a'); + } } From f816b9ff8ec6a7f10ce1884605d96692e21dd675 Mon Sep 17 00:00:00 2001 From: Ahmet Bora Date: Tue, 8 Aug 2023 22:58:30 +0300 Subject: [PATCH 17/44] Fix plugin assets with the `.mjs` extension #5463 --- config/routes.php | 2 +- tests/Cms/Plugins/PluginAssetsTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config/routes.php b/config/routes.php index 0a53526252..d8f4962221 100644 --- a/config/routes.php +++ b/config/routes.php @@ -60,7 +60,7 @@ } ], [ - 'pattern' => $media . '/plugins/(:any)/(:any)/(:all).(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)', + 'pattern' => $media . '/plugins/(:any)/(:any)/(:all)\.(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)', 'env' => 'media', 'action' => function (string $provider, string $pluginName, string $filename, string $extension) { return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension); diff --git a/tests/Cms/Plugins/PluginAssetsTest.php b/tests/Cms/Plugins/PluginAssetsTest.php index 8b735e8718..8834c75c13 100644 --- a/tests/Cms/Plugins/PluginAssetsTest.php +++ b/tests/Cms/Plugins/PluginAssetsTest.php @@ -21,6 +21,7 @@ public function setUp(): void F::write($plugin . '/index.php', 'app = new App([ 'roots' => [ @@ -54,4 +55,19 @@ public function testResolve() $this->assertSame(200, $response->code()); $this->assertSame('text/css', $response->type()); } + + public function testCallPluginAsset() + { + $response = App::instance()->call('media/plugins/test/test/test.mjs'); + + $this->assertSame(200, $response->code()); + $this->assertSame('text/javascript', $response->type()); + $this->assertSame('test', $response->body()); + } + + public function testCallPluginAssetInvalid() + { + $response = App::instance()->call('media/plugins/test/test/test.invalid'); + $this->assertNull($response); + } } From b78b54c70b87588d3200c0472621bf0c7661d316 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Sun, 13 Aug 2023 21:16:43 +0200 Subject: [PATCH 18/44] Add note on dist files to contributing guide --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 304a373241..5aadd3c9e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,7 @@ For bug fixes, please create a new branch following the name scheme: `fix/issue_ - Every bug fix should include a [unit test](#tests) to avoid future regressions. Let us know if you need help with that. - Make sure your code [style](#style) matches ours and includes [comments/in-code documentation](#documentation). - Make sure your branch is up to date with the latest state on the `develop` branch. [Rebase](https://help.github.com/articles/about-pull-request-merges/) changes before you send the PR. +- Please *don't* commit updated dist files in the `panel/dist` folder to avoid merge conflicts. We only build the dist files on release. Your branch should only contain changes to the source files. ### Features @@ -41,6 +42,7 @@ For features create a new branch following the name scheme: `feature/issue_numbe - New features should include [unit tests](#tests). Let us know if you need help with that. - Make your code [style](#style) matches ours and includes [comments/in-code documentation](#documentation). - Make sure your branch is up to date with the latest state on the `develop` branch. [Rebase](https://help.github.com/articles/about-pull-request-merges/) changes before you send the PR. +- Please *don't* commit updated dist files in the `panel/dist` folder to avoid merge conflicts. We only build the dist files on release. Your branch should only contain changes to the source files. We try to bundle features in our major releases, e.g. `3.x`. That is why we might only review and, if accepted, merge your PR once an appropriate release is upcoming. Please understand that we cannot merge all feature ideas or that it might take a while. Check out the [roadmap](https://roadmap.getkirby.com) to see upcoming releases. From 8dd1d0bc63e7267207b4f93341dc0616f3f3769d Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sat, 26 Aug 2023 17:44:00 +0200 Subject: [PATCH 19/44] Fix `$page->isUnlisted()` --- src/Cms/Page.php | 6 +-- tests/Cms/Pages/PageTest.php | 81 ++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/Cms/Page.php b/src/Cms/Page.php index 3ea0d5509f..49fa97fe13 100644 --- a/src/Cms/Page.php +++ b/src/Cms/Page.php @@ -733,7 +733,7 @@ public function isHomeOrErrorPage(): bool */ public function isListed(): bool { - return $this->num() !== null; + return $this->isPublished() && $this->num() !== null; } /** @@ -797,7 +797,7 @@ public function isSortable(): bool */ public function isUnlisted(): bool { - return $this->isListed() === false; + return $this->isPublished() && $this->num() === null; } /** @@ -811,7 +811,7 @@ public function isUnlisted(): bool public function isVerified(string $token = null) { if ( - $this->isDraft() === false && + $this->isPublished() === true && $this->parents()->findBy('status', 'draft') === null ) { return true; diff --git a/tests/Cms/Pages/PageTest.php b/tests/Cms/Pages/PageTest.php index 0146193bb1..913f3def4d 100644 --- a/tests/Cms/Pages/PageTest.php +++ b/tests/Cms/Pages/PageTest.php @@ -197,6 +197,87 @@ public function testInvalidId() ]); } + /** + * @covers ::isDraft + */ + public function testIsDraft() + { + $page = new Page([ + 'slug' => 'test', + 'num' => 1 + ]); + + $this->assertFalse($page->isDraft()); + + $page = new Page([ + 'slug' => 'test', + 'num' => null + ]); + + $this->assertFalse($page->isDraft()); + + $page = new Page([ + 'slug' => 'test', + 'isDraft' => true + ]); + + $this->assertTrue($page->isDraft()); + } + + /** + * @covers ::isListed + */ + public function testIsListed() + { + $page = new Page([ + 'slug' => 'test', + 'num' => 1 + ]); + + $this->assertTrue($page->isListed()); + + $page = new Page([ + 'slug' => 'test', + 'num' => null + ]); + + $this->assertFalse($page->isListed()); + + $page = new Page([ + 'slug' => 'test', + 'isDraft' => true + ]); + + $this->assertFalse($page->isListed()); + } + + /** + * @covers ::isUnlisted + */ + public function testIsUnlisted() + { + $page = new Page([ + 'slug' => 'test', + 'num' => 1 + ]); + + $this->assertFalse($page->isUnlisted()); + + $page = new Page([ + 'slug' => 'test', + 'num' => null + ]); + + $this->assertTrue($page->isUnlisted()); + + $page = new Page([ + 'slug' => 'test', + 'isDraft' => true + ]); + + $this->assertFalse($page->isUnlisted()); + } + public function testNum() { $page = new Page([ From a78cf803be3bcb4b74193c87ee3cf2fb80fa7945 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 27 Aug 2023 10:06:22 +0200 Subject: [PATCH 20/44] README: fix #5499 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a45e644273..15bb31b934 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Kirby is not free software. However, you can try Kirby and the Starterkit on you ### Contribute **Found a bug?** -Please post all bug reports in our [issue tracker](https://github.com/getkirby/kirby/issues). +Please post all bugs as individual reports in our [issue tracker](https://github.com/getkirby/kirby/issues). **Suggest a feature** If you have ideas for a feature or enhancement for Kirby, please use our [feedback platform](https://feedback.getkirby.com). From 712356515791667239d72a29ce712b04f91a233b Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Sun, 27 Aug 2023 14:20:46 +0200 Subject: [PATCH 21/44] Prevent `kirby` as user id --- src/Cms/User.php | 8 +- src/Cms/UserRules.php | 4 +- tests/Cms/Users/UserRulesTest.php | 18 +++- tests/Cms/Users/UserTest.php | 146 +++++++++++++++++++++--------- 4 files changed, 123 insertions(+), 53 deletions(-) diff --git a/src/Cms/User.php b/src/Cms/User.php index d9d52b1e55..47fdb88148 100644 --- a/src/Cms/User.php +++ b/src/Cms/User.php @@ -352,7 +352,7 @@ public function isAdmin(): bool */ public function isKirby(): bool { - return $this->email() === 'kirby@getkirby.com'; + return $this->isAdmin() && $this->id() === 'kirby'; } /** @@ -396,7 +396,7 @@ public function isLastUser(): bool */ public function isNobody(): bool { - return $this->email() === 'nobody@getkirby.com'; + return $this->role()->id() === 'nobody' && $this->id() === 'nobody'; } /** @@ -406,7 +406,9 @@ public function isNobody(): bool */ public function language(): string { - return $this->language ??= $this->credentials()['language'] ?? $this->kirby()->panelLanguage(); + return $this->language ??= + $this->credentials()['language'] ?? + $this->kirby()->panelLanguage(); } /** diff --git a/src/Cms/UserRules.php b/src/Cms/UserRules.php index fc3e37065f..8c6402aafc 100644 --- a/src/Cms/UserRules.php +++ b/src/Cms/UserRules.php @@ -301,8 +301,8 @@ public static function validEmail(User $user, string $email, bool $strict = fals */ public static function validId(User $user, string $id): bool { - if ($id === 'account') { - throw new InvalidArgumentException('"account" is a reserved word and cannot be used as user id'); + if (in_array($id, ['account', 'kirby', 'nobody']) === true) { + throw new InvalidArgumentException('"' . $id . '" is a reserved word and cannot be used as user id'); } if ($user->kirby()->users()->find($id)) { diff --git a/tests/Cms/Users/UserRulesTest.php b/tests/Cms/Users/UserRulesTest.php index 0b9c5e62bd..3aa1872ed3 100644 --- a/tests/Cms/Users/UserRulesTest.php +++ b/tests/Cms/Users/UserRulesTest.php @@ -364,14 +364,26 @@ public function testDeletePermissions() UserRules::delete($user); } - public function testValidId() + public function validIdProvider() + { + return [ + ['account'], + ['kirby'], + ['nobody'] + ]; + } + + /** + * @dataProvider validIdProvider + */ + public function testValidId(string $id) { $user = new User(['email' => 'test@getkirby.com']); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('"account" is a reserved word and cannot be used as user id'); + $this->expectExceptionMessage('"' . $id . '" is a reserved word and cannot be used as user id'); - UserRules::validId($user, 'account'); + UserRules::validId($user, $id); } public function testValidIdWhenDuplicateIsFound() diff --git a/tests/Cms/Users/UserTest.php b/tests/Cms/Users/UserTest.php index bf85e421d6..a2b84ad6ca 100644 --- a/tests/Cms/Users/UserTest.php +++ b/tests/Cms/Users/UserTest.php @@ -67,6 +67,107 @@ public function testInvalidEmail() $user = new User(['email' => []]); } + /** + * @covers ::isAdmin + */ + public function testIsAdmin() + { + $user = new User([ + 'email' => 'test@getkirby.com', + 'role' => 'admin' + ]); + + $this->assertTrue($user->isAdmin()); + + $user = new User([ + 'email' => 'test@getkirby.com', + 'role' => 'editor' + ]); + + $this->assertFalse($user->isAdmin()); + } + + /** + * @covers ::isKirby + */ + public function testIsKirby() + { + $user = new User([ + 'id' => 'kirby', + 'role' => 'admin' + ]); + $this->assertTrue($user->isKirby()); + + $user = new User([ + 'role' => 'admin' + ]); + $this->assertFalse($user->isKirby()); + + $user = new User([ + 'id' => 'kirby', + ]); + $this->assertFalse($user->isKirby()); + + $user = new User([ + 'emai' => 'kirby@getkirby.com', + ]); + $this->assertFalse($user->isKirby()); + } + + /** + * @covers ::isLoggedIn + */ + public function testIsLoggedIn() + { + $app = new App([ + 'roots' => [ + 'index' => '/dev/null' + ], + 'users' => [ + ['email' => 'a@getkirby.com'], + ['email' => 'b@getkirby.com'] + ], + ]); + + $a = $app->user('a@getkirby.com'); + $b = $app->user('b@getkirby.com'); + + $this->assertFalse($a->isLoggedIn()); + $this->assertFalse($b->isLoggedIn()); + + $app->impersonate('a@getkirby.com'); + + $this->assertTrue($a->isLoggedIn()); + $this->assertFalse($b->isLoggedIn()); + + $app->impersonate('b@getkirby.com'); + + $this->assertFalse($a->isLoggedIn()); + $this->assertTrue($b->isLoggedIn()); + } + + /** + * @covers ::isNobody + */ + public function testIsNobody() + { + $user = new User([ + 'id' => 'nobody', + 'role' => 'nobody' + ]); + $this->assertTrue($user->isNobody()); + + $user = new User([ + 'role' => 'nobody' + ]); + $this->assertFalse($user->isNobody()); + + $user = new User([ + 'id' => 'nobody', + ]); + $this->assertTrue($user->isNobody()); + } + public function testName() { $user = new User([ @@ -301,51 +402,6 @@ public function testValidateUndefinedPassword() $user->validatePassword('test'); } - public function testIsAdmin() - { - $user = new User([ - 'email' => 'test@getkirby.com', - 'role' => 'admin' - ]); - - $this->assertTrue($user->isAdmin()); - - $user = new User([ - 'email' => 'test@getkirby.com', - 'role' => 'editor' - ]); - - $this->assertFalse($user->isAdmin()); - } - - public function testIsLoggedIn() - { - $app = new App([ - 'roots' => [ - 'index' => '/dev/null' - ], - 'users' => [ - ['email' => 'a@getkirby.com'], - ['email' => 'b@getkirby.com'] - ], - ]); - - $a = $app->user('a@getkirby.com'); - $b = $app->user('b@getkirby.com'); - - $this->assertFalse($a->isLoggedIn()); - $this->assertFalse($b->isLoggedIn()); - - $app->impersonate('a@getkirby.com'); - - $this->assertTrue($a->isLoggedIn()); - $this->assertFalse($b->isLoggedIn()); - - $app->impersonate('b@getkirby.com'); - - $this->assertFalse($a->isLoggedIn()); - $this->assertTrue($b->isLoggedIn()); - } public function testQuery() { From 556b0ffb0602d858d272d74146ab8014eadf8790 Mon Sep 17 00:00:00 2001 From: Nico Hoffmann Date: Tue, 29 Aug 2023 19:36:12 +0200 Subject: [PATCH 22/44] List field: fix disabled writer Fixes #5474 --- panel/src/components/Forms/Input/ListInput.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panel/src/components/Forms/Input/ListInput.vue b/panel/src/components/Forms/Input/ListInput.vue index fd6caee4a8..07950b0a97 100644 --- a/panel/src/components/Forms/Input/ListInput.vue +++ b/panel/src/components/Forms/Input/ListInput.vue @@ -11,9 +11,11 @@