diff --git a/src/Sites/Site.php b/src/Sites/Site.php index 7f0b4123fb8..430045940be 100644 --- a/src/Sites/Site.php +++ b/src/Sites/Site.php @@ -119,6 +119,10 @@ protected function resolveAntlersValue($value) ->all(); } + if (! is_string($value) || ! Str::contains($value, ['{', '@'])) { + return $value; + } + $value = Parse::config($value); $isEvaluatingUserData = GlobalRuntimeState::$isEvaluatingUserData; diff --git a/src/Sites/Sites.php b/src/Sites/Sites.php index aaec4b29c24..d00c0cd65e8 100644 --- a/src/Sites/Sites.php +++ b/src/Sites/Sites.php @@ -39,6 +39,10 @@ public function all() public function authorized() { + if (User::current()->isSuper()) { + return $this->sites; + } + return $this->sites->filter(fn ($site) => User::current()->can('view', $site)); } diff --git a/src/Structures/Nav.php b/src/Structures/Nav.php index ce901447c03..dc3a38c0e1b 100644 --- a/src/Structures/Nav.php +++ b/src/Structures/Nav.php @@ -145,7 +145,7 @@ public function sites() public function existsIn($site) { - return $this->trees()->has($site); + return $this->in($site) !== null; } public function blueprint() diff --git a/tests/Performance/SitesPerformanceTest.php b/tests/Performance/SitesPerformanceTest.php new file mode 100644 index 00000000000..d61c114c57d --- /dev/null +++ b/tests/Performance/SitesPerformanceTest.php @@ -0,0 +1,115 @@ +setSites([ + 'en' => ['url' => 'http://test.com/'], + 'fr' => ['url' => 'http://fr.test.com/'], + 'de' => ['url' => 'http://test.com/de/'], + ]); + + // Create and act as a super user + $superUser = tap(User::make()->id(1))->save(); + $superUser->makeSuper()->save(); + $this->actingAs($superUser); + + // Super user should get all sites without any filtering + $authorizedSites = $sites->authorized(); + + $this->assertEquals(3, $authorizedSites->count()); + $this->assertEquals(['en', 'fr', 'de'], $authorizedSites->keys()->all()); + } + + /** + * Test that regular users still get filtered authorization check. + * This ensures regular users only see sites they have access to. + */ + #[Test] + public function regular_user_gets_filtered_site_authorization() + { + $sites = (new Sites)->setSites([ + 'en' => ['url' => 'http://test.com/'], + 'fr' => ['url' => 'http://fr.test.com/'], + 'de' => ['url' => 'http://test.com/de/'], + ]); + + Role::make('editor') + ->permissions([ + 'access en site', + 'access fr site', + ]) + ->save(); + + $regularUser = tap(User::make()->assignRole('editor'))->save(); + $this->actingAs($regularUser); + + \Statamic\Facades\Site::shouldReceive('multiEnabled')->andReturnTrue(); + \Statamic\Facades\Site::shouldReceive('all')->andReturn(collect()); // CorePermissions calls this + + // Regular user should only get sites they have access to + $authorizedSites = $sites->authorized(); + + $this->assertEquals(2, $authorizedSites->count()); + $this->assertEquals(['en', 'fr'], $authorizedSites->keys()->all()); + } + + /** + * Test that site configuration with plain strings doesn't trigger Antlers parsing. + * This ensures performance optimization for non-template string values. + */ + #[Test] + public function plain_string_site_config_skips_antlers_parsing() + { + // Test with a simple string URL that has no template syntax + $site = new Site('en', [ + 'url' => '/en/', + 'locale' => 'en_US', + 'name' => 'English', + ]); + + // These should be plain strings, not parsed by Antlers + // Note: URLs are tidied, so trailing slash is removed + $this->assertEquals('/en', $site->url()); + $this->assertEquals('en_US', $site->locale()); + $this->assertEquals('English', $site->name()); + } + + /** + * Test that site configuration with Antlers syntax still gets parsed. + * This ensures the optimization doesn't break actual template parsing. + */ + #[Test] + public function site_config_with_antlers_syntax_gets_parsed() + { + // Test with template syntax that should be parsed + $site = new Site('en', [ + 'url' => '/', + 'locale' => '{{ config:app.locale }}', + 'name' => 'Test Site', + ]); + + // Locale should have been parsed + $this->assertNotEmpty($site->locale()); + // It should not contain the template syntax anymore + $this->assertStringNotContainsString('{{', $site->locale()); + } +} diff --git a/tests/Structures/NavPerformanceTest.php b/tests/Structures/NavPerformanceTest.php new file mode 100644 index 00000000000..c3bc25b7d43 --- /dev/null +++ b/tests/Structures/NavPerformanceTest.php @@ -0,0 +1,73 @@ +app->make('stache'); + $stache->store('nav-trees')->directory($this->directory = $this->fakeStacheDirectory.$this->directory.''); + + $this->setSites([ + 'en' => ['locale' => 'en_US', 'url' => '/'], + 'fr' => ['locale' => 'fr_FR', 'url' => '/fr/'], + 'de' => ['locale' => 'de_DE', 'url' => '/de/'], + ]); + } + + /** + * Test that existsIn() efficiently checks for nav in a specific site. + * This verifies the performance optimization that uses in() instead of trees(). + */ + #[Test] + public function exists_in_checks_nav_for_specific_site() + { + $nav = tap(Nav::make('links'))->save(); + + // Create a tree for only the 'en' site + $nav->makeTree('en', [['title' => 'Home', 'url' => '/']])->save(); + + // Nav should exist in 'en' site + $this->assertTrue($nav->existsIn('en')); + + // Nav should not exist in 'fr' site + $this->assertFalse($nav->existsIn('fr')); + + // Nav should not exist in 'de' site + $this->assertFalse($nav->existsIn('de')); + } + + /** + * Test that existsIn() works when nav exists in multiple sites. + */ + #[Test] + public function exists_in_checks_nav_across_multiple_sites() + { + $nav = tap(Nav::make('links'))->save(); + + // Create trees for 'en' and 'fr' sites + $nav->makeTree('en', [['title' => 'Home', 'url' => '/']])->save(); + $nav->makeTree('fr', [['title' => 'Accueil', 'url' => '/']])->save(); + + // Nav should exist in both 'en' and 'fr' sites + $this->assertTrue($nav->existsIn('en')); + $this->assertTrue($nav->existsIn('fr')); + + // Nav should not exist in 'de' site + $this->assertFalse($nav->existsIn('de')); + } +}